Skip to content

9.5 Git 高级操作

掌握了前面介绍的 Git 基础、分支管理、远程仓库的知识,熟悉了基本的操作命令,你已经有能力应对日常的开发工作了。然而 Git 的能力不止这些,对于追求高效的开发人员来说,Git 提供了许多更高级的操作,这些操作要求你对 Git 原理有更深层次的理解。

9.5.1 git rebase

rebase 译为 “变基”,在回退提交时介绍过 “git rebase -i” 的用法,现在我们继续探索这个命令。

在 Git 中合并不同分支的提交有两个方法:一个是 merge,另一个就是 rebase。要搞懂 rebase 的独到之处,我们先了解两者合并方式的本质区别。

merge 解析

大多数合并会使用 merge,它的原理是将两个分支的最新快照(C3 和 C4)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(C5),如图所示。

从图中可以看出,merge 合并不同分支的提交会自然产生分叉,并生成新的提交。也就是说,它将两个分支的不同提交保留,然后在新的提交上标记合并后的更改。

这种方式看起来有两个不优雅之处:

  1. 提交记录产生了分叉。
  2. 生成了新的提交。

rebase 解析

针对 merge 的不优雅之处,我们用一种更简洁的方法处理。假设现在要将 master 分支与 experiment 分支合并,它们的最新提交分别是 C3 和 C4,处理方法如下:

  1. 尝试将 C4 直接并入 master 分支的提交线,根据提交时间决定 C4 位于 C3 之前还是之后。
  2. 如果并入时产生冲突,首先解决冲突并暂存,然后重新并入。

上述的两个步骤,就是变基(rebase)操作的合并流程。这个过程没有产生分叉,没有产生新提交,只是在有冲突的时候解决冲突重新合并,这样直接将两个分支的修改组合了起来。

变基操作顾名思义,就是改变基线。我们在当前分支(experiment)变基 master 分支,本质上就是以 master 分支为基准,将当前分支存在修改的提交合并上去。当前分支可以有多个提交,合并时会依次验证冲突并依次合并,最终将处理过的 master 提交线重新应用到 experiment 分支,此时变基完成。

变基(rebase)可以让分支始终保持一个简洁干净的提交线,没有分叉,没有多余提交,这让我们可以非常清晰直观地看到提交过程。rebase 与 merge 的执行结果始终是一样的,它们只有提交历史的差别。

rebase 恢复

使用变基可以使提交历史保持一条直线,便于历史记录的追踪。但是 rebase 的优雅之处也藏着一丝风险,因为 rebase 会修改分支的基线,重新合并新的提交。当分支存在分叉时,合并就是出现异常。

请记住一条黄金定律:merge 和 rebase 不要混用。如果你的团队使用 merge 合并,那么一直使用 merge 好了;如果团队使用 rebase,那么请规范所有的合并都用 rebase,避免带来不可预期的错误。

我们介绍过,在使用 pull 拉代码时,默认会使用 merge 合并到本地。如果我们统一使用 rebase,那么拉代码时请指定使用 rebase 合并,命令如下:

sh
$ git pull --rebase origin master

当执行变基操作后,因为基线被重置,因此无法使用 “git log” 来查到变基之前的提交。如果此时我们想恢复到变基之前的代码,需要使用 “git reflog” 查到变基前的最新 commitId,然后使用 “git reset” 重置。

9.5.2 git cherry-pick

不管是 rebase 还是 merge,它们都是把两个分支的所有提交合并,但有时候我们并不想这么做。比如在某个分支上创建了一个新提交,现在我只想把这一个提交合并到 master 分支,而并非是将整个分支合并,此时就用到了 “git cherry-pick” 命令。

cherry-pick 的中文释义是精心挑选,一个一个地挑,这个释义与它的功能非常贴合。cherry-pick 的作用就是提取单个提交并将其追加在某个分支上,这样实现了更灵活更细粒度的提交管理。

假设当前分支是 master,现在有一个提交(可以在任意分支上)的 commitId 为 fh583g4,那么将其追加到 master 的提交上,方式如下:

sh
$ git cherry-pick fh583g4

cherry-pick 还可以一次性追加多个提交,这使我们在需要批量操作时更快捷。但是请注意:使用 cherry-pick 追加多个提交时,要按照时间顺序追加,不可以乱序。

比如,A 分支上按时间从早到晚有 a,b,c,d 四个提交,我们要合并这四个提交,请按照顺序书写:

sh
$ git cherry-pick a b c d

如果你是追加连续的几个提交,也可以用范围指定的便捷写法:

sh
$ git cherry-pick a..d

如果执行 cherry-pick 的过程中发生冲突,此时有三种解决方案:

  1. 跳过冲突:“git cherry-pick --skip”。
  2. 放弃执行:“git cherry-pick --abort”。
  3. 解决冲突后继续执行:“git cherry-pick --continue”。

这里的命令参数与 rebase 是一致的,解决冲突后添加到暂存区并继续执行即可。

9.5.3 git stash

你可能遇到这种情况:临时切换分支或者拉取最新代码,但你本地做了修改,且不需要提交,这个时候就可以把代码变更临时存储在一个地方,这就是 “git stash” 的作用。

stash 的作用是临时存储。但是注意:请不要将 stash 与暂存区的概念混淆。“git stash” 只是将未提交的文件暂时“隐藏”在某个地方,使它不出现在工作区和暂存区;而暂存区保存的是我们下一次要提交的代码。

将本地代码的变更临时存储,使用以下命令:

sh
$ git stash

此时会发现,工作区的修改消失了。执行完切分支或拉代码的操作后,我们再把暂存起来的内容还原:

sh
$ git stash pop

这样,临时存储的代码又会回到工作区。很显然 stash 是将每一次临时存储的文件保存在一个栈中,当我们恢复文件时,相当于执行了一次出栈操作,因此恢复文件只能从最近的保存依次恢复。

注意:stash 只是一个临时保存的方案,当你执行完需要的操作后,请立即恢复文件。笔者的经历来看,如果没有立即恢复很可能会忘记这次临时存储,从而导致代码丢失。

stash 只能存储被 Git 跟踪的文件,如果是新建文件,请确保 “git add” 之后才能被存储。

9.5.4 git grep

有时候我们需要在项目代码中查找一个字符或者一个函数,通常的做法是在编辑器中全局搜索,编辑器会列出匹配到的所有文件。但如果我们只是在终端进入一个项目目录,而并不想用编辑器打开,此时应该怎么做?

这里就要用到 Git 提供的搜索命令:“git grep”。grep 命令可以从当前工作目录或提交记录中检索一个指定的值,该值可以是一个字符串,也可以是更灵活的正则表达式。

比如说我们要查找项目代码中有没有 “a” 这个字符串,命令如下:

sh
$ git grep a
src/a.js:var a = 3;
src/b.js:var a = 7;
src/d.js:var a = 4;
src/d.js:console.log(a);

上面的命令执行之后,会列出匹配的三条结果,每条结果会展示出匹配的文件路径和具体代码。如果要展示匹配代码在文件中的行号,可以加一个 -n 参数:

sh
$ git grep -n a
src/a.js:1:var a = 3;

此时单条查询结果的格式为:<文件路经>:<行号>:<具体代码>。

当然如果你要查询一个大量使用的值,此时列出所有匹配的代码可能并不合适。也许你只是想统计该字符串在文件中出现了多少次。此时可以直接使用 -c 参数来实现,如下:

sh
$ git grep -c a
src/a.js:1
src/b.js:1
src/d.js:2

默认情况下,git grep 只会检索当前工作目录的文件。我们还可以指定搜索任意 Git 树,比如要检索另一个分支或某个标签下的文件,命令如下:

sh
# 检索 dev 分支
$ git grep a dev
# 检索 v1.0.1 标签
$ git grep a v1.0.1

9.5.5 git bisect

我们知道 Git 主要用于版本控制,管理项目文件。但也许你不知道,Git 还提供了调试功能来协助处理代码当中的问题。因为 Git 会一直跟踪文件的修改,记录变更历史,因此可以很容易帮你找到问题所在。

如何使用 Git 调试代码呢?根据不同的情况,主要有以下两种方式。

文件标注

当我们判断可能是某个文件出现了问题,那么可以直接追踪该文件的变更历史,查找是否有异常来源。Git 提供了文件标注功能来实现这个需求。文件标注使用 “git blame” 命令实现,该命令可以查看到文件中每一行的修改时间和对应的 commitId。

假设某个功能昨天是正常的,今天突然出错了,那么就可以通过文件标注来查看每行代码的更新时间,找到最近更新的那几行,再确定对应的 commitId 之后,我们直接查看提交详情的代码变更就好了。

比如现在要标注 package.json 这个文件,使用命令和执行结果如下:

sh
$ git blame package.json
^ac8fecf (ruidoc 2021-11-08 15:07:11 +0800  1) {
e04127d7 (ruidoc 2022-03-18 16:35:33 +0800  2)   "name": "git-demo",
c9481d36 (ruidoc 2022-06-01 19:55:09 +0800  3)   "version": "0.1.0",
cd84e127 (ruidoc 2022-03-22 20:08:25 +0800  4)   "homepage": "/",
^ac8fecf (ruidoc 2021-11-08 15:07:11 +0800  5)   "private": true,
...

从上述结果来看,逐行打印出了 package.json 的代码,并在代码前分别显示了 commitId、提交人、提交时间。其中以 “^” 符号开头的提交表示初始提交,即没有变更过的代码。通过时间可以筛选出最近修改记录,然后找到文件变更的源头。

有时候可能某个文件很大,我们可以通过 -L 参数指定输出的行数。比如只需要显示前 10 行,命令如下:

sh
$ git blame -L 1,10 package.json

二分查找

另一种场景是:某个功能突然出错了,但我们不确定是哪个文件导致的错误,此时就可以对提交历史进行二分查找排查错误。二分查找使用命令 “git bisect” 实现。

二分查找是 Git 使用算法排查错误的方式。假设最近的一次提交 c 正常,我们首先执行以下三个步骤:

    1. 启动排查:“git bisect start”
    1. 标记当前提交异常:“git bisect bad”
    1. 标记最近一次正常的提交:“git bisect good c”

经过上面的三个步骤,我们已经将异常锁定在当前提交和正常提交 c 之间。假设这两个提交之间还有 10 个提交,那么异常一定出现在这 10 个提交之间的某一个。

三个步骤执行完毕,Git 会自动检出 10 个提交中间的那一个。现在我们继续测试异常还在不在,如果还存在,说明问题是在这个提交之前;否则,问题是在这个提交之后。这样,异常范围就缩小了一半,这就是二分查找。

使用以下命令来标记当前提交状态。执行之后,会自动检出异常提交中间的那一个,进入下一轮二分查找。

sh
$ git bisect good # 标记当前提交正常
$ git bisect bad # 标记当前提交异常

当 Git 找到了出现异常的提交之后,会自动在终端输出提交详情信息,我们直接查看这个提交的代码变更,排查问题就可以了。

当完成这些后,记得恢复调试开始前的提交,因为调试时指针会随时变化。恢复方法如下:

sh
$ git bisect reset

本章小结

本章从版本控制的角度入手,带大家了解了为什么需要 Git、以及 Git 的工作原理是什么。在对 Git 有了基础认识之后,接着介绍了 Git 的基本概念和常用命令,使我们学会了简单地用 Git 管理项目。后来又介绍了远程仓库、本地与远程之间的文件交换,这会帮助我们用 Git 进行团队协作,此时你已经可以应对日常的开发工作了。

尽管如此,我们在最后还介绍了一部分高级操作 ——— 这些操作虽不是必须掌握,但是偶尔使用它们确实可以高效地解决问题。本篇介绍的 Git 知识内容并不算少,但这还不是 Git 的全部。更多地掌握 Git 还可以在未来参与开源项目建设,是作为一个高级程序员的必备技能!