# Git 常用技巧

Git 中有很多实用的技巧, 让提交代码变成更愉悦的事, 本文将持续记录作者积累的一些技巧

# ~^ 的区别

# Commit Log

avatr

每条线上的雪花点右侧对应的 commitid,都属于该条线,开始和结尾的公共点共同拥有, 可以使用--decorate--graph等参数简化 git 提交历史图

git log --graph --decorate

上图第一条线拥有 165f5a1->3cb9272->de87e10->b6de943这几条提交记录:

下面是等价的表示方法:

G H I J
\ / \ /
D E F
\ | / \
 \ | / |
\|/ |
B C
\ /
\ /
A

A = = A^0
B = A^ = A^1 = A~1
C = A^2
D = A^^ = A^1^1 = A~2
E = B^2 = A^^2
F = B^3 = A^^3
G = A^^^ = A^1^1^1 = A~3
H = D^2 = B^^2 = A^^^2 = A~2^2
I = F^ = B^3^ = A^^3^
J = F^2 = B^3^2 = A^^3^2

# HEAD^[num]

用于选择第几条为主线,例如使用:

git reset --hard HEAD^2

这个将从左往右选取第二条为主线,且回退一个版本,commit-id 将为:

ae3d768->02c1b1b->b6de943

# HEAD~[num]

用于在当前主线『默认 master』,回退版本,例如使用:

git reset --hard HEAD~1

这个将选取当前 master 主线,且回退一个版本,commit-id 将为:

3cb9272->de87e10->b6de943

这种使用方式 HEAD 后面可以跟多个^,每个表示回退一个版本,效果同 HEAD~[num]

git reset --hard HEAD^^

这个将选取第一条主线,且回退两个版本,commit-id 将为:

de87e10->b6de943

# HEAD~~

这种使用方式 HEAD 后面可以跟多个~,每个表示回退一个版本,效果同 HEAD~[num]

git reset --hard HEAD~~

这个将选取第一条主线,且回退两个版本,commit-id 将为:

de87e10->b6de943

# 如何选用

波浪符号~在外观上几乎是线性的,并且想沿直线向后走; 而插入符号^表示一棵有趣的树段或道路上的一个岔路口

  • 大部分时间使用~——回溯几代,通常是你想要的
  • 用于^合并提交——因为它们有两个或更多(直接)父级

# 代码统计

如果想了解多人团队中同事或者自己的代码统计情况, git提供了相关的命令, 方便且直观

# 查看团队每个人的代码提交量
git shortlog -s -n

# Fast-Forward 合并

Git 中的 fast-forward(快速前进)是一种合并(merge)分支的方式,它通常用于将一个分支的更改合并到另一个分支上。

Fast-forward 合并的效果是,目标分支(主分支)的指针直接移动到源分支(开发分支)的最新提交,因此看起来就像是主分支“快速前进”到了开发分支的状态

Fast-forward 合并通常是一种简单且干净的合并方式,因为它不会创建合并提交(merge commit),但它只适用于特定的情况,即主分支没有新的提交。如果主分支有新的提交,你可能需要执行普通的合并,这将创建一个合并提交以整合来自其他分支的更改

`Fast-forward 合并`只会发生在特定条件下

  • 合并到主分支

假设你有一个开发分支,比如 feature-branch,并且你在该分支上做了一些更改。当你想将这些更改合并到主分支(通常是 master 分支)时,如果在合并时没有新的提交到主分支,Git 将执行 fast-forward 合并

  • 没有冲突

fast-forward 合并中,没有冲突会发生。这是因为在 fast-forward 合并时,Git 简单地将目标分支(主分支)指向源分支(开发分支)的最新提交,这不会导致冲突

更多相关的内容可以查阅Merging vs Rebasing (opens new window)

# 变基 Rebase

笔者总结了rebase的最常用三种用法(注意区间前开后闭):

# 合并 commit

在开发中, 可能自己的分支进行了多次的提交, 为了保持合入公共开发分支中自己的 commit 的整洁性, 可以使用 commit 合并

注意

一般使用场景是自己的本地分支, 最好不要在远端公共分支上进行这种操作, 是极其危险的

# 区间前开后闭, 一般可以使用 commitid~1来包含该提交
git rebase -i  <start-commit>  <end-commit>
# 此时, 会进入一个匿名分支, 需要切出一个新分支
git checkout -b <name>
# 推送到自己的远端
git push origin HEAD

# 修改 commit

如果前几次已经git commit到本地, 此时想修改他们提交信息, 也可以是用rebase -i命令

# 例如 git rebase -i HEAD~1 就是修改最近一次提交
git rebase -i <start-commit>  <end-commit>

此时在vim模式下, 与合并commit所选择使用的pick选项不同, 而是选择使用reword选项来修改提交信息。

rebase -i 支持的选项有下图中的几种

修改提交信息

特殊情况

如果只修改最新一次的提交信息, 可以使用git commit --amend命令

此时等同于git rebase -i HEAD~1

# 复制 commit

可以复制某一个分支的一串 commit 到当前分支, 类似于cherry-pick

  • base: 分支名称
  • from: 待合并片段的起始 commitId(不包含)
  • to: 待合并片段的结束 commitId(包含)

语法:

git rebase --onto base from to

# 被复制的分支上执行
git checkout -b  <new-branch>  <end-commit>

git rebase --onto  <target-branch>  <start-commit>^

# 合并远端分支

拉取远端代码的具体分支时, 拉取后与本地进行fast-forward合并, 可以使用

git pull --rebase origin <target-branch>

# 重设分支基点

团队协作中, 主分支经常会更新一些内容, 因此我们提交到主分支前一般都需要先合并主分支的内容到开发分支, 主要使用两种方式合并

如果使用merge来合并主分支, 那么会产生一条多余的无关合并记录, 从而污染了开发分支, 如图

Merging-main-into-feature.svg

为了解决上述的问题, 就得使用rebase重设开发分支的基点, 使得开发分支的提交记录变得干净

git checkout feature
git rebase main

这样就将整个feature分支从分支的顶端开始,有效地将所有新提交合并到main。 但是,变基不是使用合并提交,而是通过为原始分支中的每个提交创建全新的提交来重写项目历史记录

合并后的分支图如下

Rebasing-the-feature-branch-into-main.svg

# 删除本地无效的分支

远端有很多分支已经被删除, 而本地仍然存在, 删除本地的无效的分支可以使用prune命令

# 列出已经失效的引用分支
git remote prune show  origin

# 删除失效的分支
git remote prune origin

# 恢复被删除的 stash 代码

有时候不小心清空了stash list中的备用代码, 想找回来怎么办? 可以使用以下命令:

# 撤销git stash clear的操作
git fsck --unreachable | grep commit | cut -d ' ' -f3 | xargs git log --merges --no-walk --grep=WIP

# Git Reset 的三个选项

  • mixed(默认值)

回退一个版本,且会将暂存区的内容和本地已提交的内容全部恢复到未暂存的状态,不影响原来本地文件及未提交的本地修改

git reset (-–mixed) HEAD~1
  • soft

回退一个版本,不清空暂存区,将已提交的内容恢复到暂存区,不影响原来本地的文件(未提交的也不受影响)

git reset -–soft HEAD~1
  • hard

回退一个版本,清空暂存区,将已提交的内容的版本恢复到本地,本地的文件也将被恢复的版本替换

git reset -–hard HEAD~1

# Git 恢复误删的分支

  • 使用 git log -g 找回之前提交的 commit_id

  • 使用 git branch recover_branch[新分支] commit_id 命令用这个 commit 创建一个分支

  • 切换到 recover_branch_abc 分支,检查文件是否存在

# Git 图片提交失败

在某些情况下, Git提交成功并推送到远端后, 发现远端分支的图片并没有更新, 这种情况下需要使用-- force 或者 -- refresh选项来进行add

git add --refresh .
# 或者
git add --force .

# 回滚本地分支

# git reset (--mixed) [commit id]

回退到指定版本,且会将暂存区的内容和本地已提交的内容全部恢复到未暂存的状态,不影响原来本地文件(未提交的也不受影响) 该参数--mixedgit reset的默认参数 例如需要回退一个版本, 执行下面命令:

# 回退一个版本
git reset HEAD~1

需要回退到某个 commitid(提交id通过git log查看), 如需要回滚到4a50c9f,则执行

git reset 4a50c9f

# git reset --soft [commit id]

回退到指定版本,不清空暂存区,将已提交的内容恢复到暂存区,不影响原来本地的文件(未提交的也不受影响)

# git reset --hard [commit id]

回退到指定版本,清空暂存区,将已提交的内容的版本恢复到本地,本地的文件也将被恢复的版本替换

# git revert [commit id]

生成一个新的 commit,将指定的 commit 内容从当前分支上撤除

# git revert -m [parent Id][commit id]

当需要 revert 回滚两个分支合并后的一个公共提交, 此时需要加上-m选项来确认是第几个父 id(也就是确认回滚哪一条分支); 可以通过git show [提交id] 来查看有几个父 id

例子: 回滚到 cad132423 这个公共提交的第一个父节点 git revert -m 1 cad132423

# 回滚远端分支

案例: 需要回滚 master 上面的代码到 4a50c9f

# 第一种方法

以下是步骤:

  • 先回滚到需要移除的 comitid 的前一次正确 commitid
git checkout -b remote-v1 4a50c9f
  • 合并策略为强行保留现在的分支(假合并) 合并中完全采用 remote-v1 的代码
git merge -s ours master
  • 推送到远程分支
# 也可以使用 git push origin HEAD:master
git push origin remote-v1:master

# 第二种方法

以下是步骤:

  • 先移除有代码错误的 comitid, 撤销一连串的 id 用commidId..commitId, 参数--no-commit是用于后面手动提交
git revert --no-commit f7742cd..551c408
  • 正常提交代码
git commit -a -m 'This reverts commit 7e345c9 and 551c408'
git push origin HEAD:master

# 第三种方法[极不推荐]

该方法会强行覆盖远端分支的代码, 稍微不慎, 会导致其他人的代码丢失

以下是步骤:

  • 使用文章开头的方式回滚本地分支
  • 强行提交到远端分支
git push origin master -f

## 未完待续

# 参考资料