Skip to content
This page has been auto-translated and may contain errors.View in English

Git 中的分支 ​

docs.scrimba.com

假设你正在开发 weather-app,写到一半时想试着加一个五日天气预报功能。这个功能可能要提交好几次才能调好,在调好之前,你肯定不想让那些没写完的代码,跟已经在正常给用户使用的那部分代码混在一起。分支正是 Git 用来解决这个问题的办法:开一条独立的工作线来放这个实验性功能,而 main 分支会一直保持你开始改动之前的样子,正常工作。

分支到底是什么 ​

来看看它实际是怎么用的:

bash
$ git branch forecast
$ git switch forecast
Switched to branch 'forecast'

两条命令:第一条创建一个叫 forecast 的新分支,第二条把你切换到这个分支上。从这里开始,你做的每一次提交都会落在 forecast 上,而 main 会一直待在原地不动。

分支 本质上就是一个指向某次提交的、可以移动的标签。现在,main 和 forecast 都指向同一个提交——也就是你运行 git branch forecast 时所在的那个提交。一旦你在 forecast 上再次提交,这个标签就会往前移动到新的提交上。而 main 会一直待在原来的位置,直到你切换到它上面并亲自在那里提交。

用 git branch 可以同时看到这两个标签:

bash
$ git branch
  forecast
* main

星号标记的是你当前所在的分支。分支说到底就是一个指向某次提交的名字。这就是它的全部原理。这一章接下来讲的内容,本质上都是围绕着如何移动这些标签展开的。

在实际项目中,分支名是有含义的。一种常见的约定是:先写一个简短的前缀说明改动的类型,后面跟一段描述,比如 feature/five-day-forecast、fix/broken-date-picker、chore/upgrade-eslint。有些团队还会加上工单号(比如 feature/wa-142-forecast)。不管你选哪种约定,都要在整个项目里保持一致,这样任何人不用打开分支就能看出它是干什么用的。

这类命名方式对应的是“短命的功能分支”这种工作流程:为一项工作创建一个分支,提交代码,让改动经过评审并合并,然后把分支删掉。一个存活了好几周的分支会离 main 越来越远,拖的时间越长,最后合并起来就越麻烦。最健康的分支通常几天之内就能完工。

分支本质上不过是一个存着单个提交哈希值的小文件,藏在 Git 自己的记录信息里。创建一个分支,无非就是写下这一行内容,这也是为什么即便是在有着多年历史的仓库里,git branch 也能瞬间执行完。正因为分支这么“廉价”,命名约定和短命分支这些做法在实践中才显得重要:删掉一个过时的分支同样不花 Git 任何代价,所以除了团队自身的自律,没有什么能阻止废弃分支越堆越多。

Juno分支到底是什么git branch forecast 会创建一个指向你当前提交的新标签,而 git switch forecast 会把你切换到这个标签上。在你真正提交之前,你的文件不会有任何变化。我喜欢把分支想象成贴在某次提交上的便利贴:添加起来很便宜,移动起来很便宜,用完撕掉也很便宜。
Juno分支到底是什么 分支名说到底就是一个指针,所以创建它不花什么代价,切换起来也是瞬间完成。给分支起一个能说明工作内容的名字,比如 feature/five-day-forecast,并且让它保持短命:创建、提交、合并、删除。真正让一次普通合并变成大麻烦的,往往是那些存活太久的分支。
Juno分支到底是什么 分支真的就是一个存着提交哈希值的小文件,这也是为什么创建分支是 Git 里最便宜的操作之一。命名约定和短命分支是团队用来让并行工作保持清晰可读的手段;Git 本身并不会强制执行这些规则。我清理过太多六个月前留下的废弃分支了,所以对这件事有很坚定的看法。

创建和切换分支 ​

单独运行 git branch forecast 只会创建这个标签,不会把你切换过去。大多数时候你想要一步到位,所以 git switch 提供了一个快捷方式:

bash
$ git switch -c forecast
Switched to a new branch 'forecast'

-c 代表“create”(创建)。git switch -c 会同时创建分支并把你切换过去。几乎每次开始新工作时,你都会用这一条命令。

切换分支会改变你的文件呈现出的是哪个快照。试试看:下面的 echo 命令是快速创建新文件 src/forecast.js 并写入一行内容的简便写法,其余的都是你已经熟悉的命令:

bash
$ git switch forecast
$ echo "// 五日预报逻辑" > src/forecast.js
$ git add src/forecast.js
$ git commit -m "Start forecast module"
$ ls src
forecast.js  index.js
$ git switch main
Switched to branch 'main'
$ ls src
index.js

这个文件并没有消失。它一直存在于 forecast 分支上,只是 main 分支的快照从来就没包含过它。切回 forecast,它就会原样重新出现。

在已有的代码和教程里,你会看到一个更老的说法:git checkout forecast 做的事情跟 git switch forecast 是一样的。checkout 出现得更早,而且它身兼数职:根据你给它的参数不同,它可以切换分支,也可以把某个文件恢复到早先的版本,光看命令名本身,你没法判断它接下来要做哪一件事。switch 和 restore 把这两项工作拆成了两个独立的命令,这样命令名本身就能一目了然:switch 负责在分支之间切换,restore 负责把文件恢复到某次提交时的样子。日常使用请用新的这两个命令;但在旧代码里遇到 checkout 时,要能认出它是干什么的。

checkout 会根据你传给它的东西来决定要做什么:传一个分支名就切换分支,传一个文件路径就恢复那个文件,如果某个名字两边都能对上,Git 就得去猜你到底想要哪一种。正是这种“猜”的问题,才让这个命令被拆成了两个。switch 只会切换分支,restore 只会改动文件,这样一来,一个打错的路径就不会再意外把你切换到某个分支上了。可以预见,未来很多年里,旧脚本和旧教程还会继续用 checkout;但新写的东西,请用 switch 和 restore。

Juno创建和切换分支git switch -c forecast 一步创建分支并切换过去,这也是你几乎每次都会用到的方式。切换分支会换掉你看到的文件版本,所以在一个分支上新加的文件,在另一个分支上是看不到的,除非你切回来。什么都没丢,只是暂时停在另一个分支上而已。
Juno创建和切换分支git switch -c 名字 一步完成创建和切换。在老一点的教程和代码库里,你会看到 git checkout 名字 做着同样的事;它其实是同一个思路,只是套在一个更老、功能更多的命令里。日常操作用 switch,看到 checkout 也能不假思索地读懂它。
Juno创建和切换分支checkout 既能切换分支,也能恢复文件,具体做哪件事取决于它收到的参数——而这正是 switch 和 restore 被拆分出来要消除的那种歧义。可以预见,未来多年里旧脚本和旧教程还会继续出现 checkout;它依然能用,只是不再是默认该写的那个命令了。

HEAD 和分离头指针 ​

Git 用一个叫 HEAD 的标记来记录你当前所在的位置。通常情况下,HEAD 指向你当前所在的分支,而那个分支又指向某次提交,所以每次你切换分支或提交时,HEAD 都会自动跟着移动。但如果你不是通过分支名,而是通过哈希值检出某个具体的提交,或者检出一个标签(tag,一种固定钉在某次提交上的标签,常用来标记发布版本),HEAD 就会直接挂到那一个提交上,而不再挂在某个分支标签上。这种状态被称为“分离头指针”(detached HEAD)。

在这种状态下,你可以随意浏览,甚至可以提交,但没有任何名字指向这些新提交。如果你不先创建一个分支就切换到别的分支上去,这些提交就会变得很难再找回来。

bash
$ git switch a1b2c3d
HEAD is now at a1b2c3d Add password strength meter

“HEAD is now at”这条提示信息,就是 Git 在告诉你:你已经离开了分支的地界,HEAD 现在直接挂在了某一个提交上。

在日常工作中,你很少会不小心陷入这种状态。它最常出现在这些情况:检出一个旧标签或某个具体提交,去看看代码在那个时间点的表现;或者某个构建工具检出某一个确切的提交来进行构建。如果你只是想看看,那没问题:浏览文件、跑跑程序,看完之后切回某个分支就行。但如果在分离头指针状态下,你做出了值得保留的改动,那就要在切走之前先创建一个分支:在那个分离状态下运行 git switch -c fix/old-bug,就能给这次提交一个真正的名字,让它在切换之后也能存活下来。

在分离头指针状态下留下的提交,并不会在你切走的那一刻就被删除。它会变成不可达(unreachable)状态:仍然存储在仓库里,只是不再有任何分支、标签或 HEAD 指向它。Git 也不会立刻把这些清理掉。它会保留一份 reflog,记录 HEAD 曾经指向过的每一个位置,一个不可达的提交通常能存活好几周,之后 Git 才会真正把它彻底删除。

这给了你充足的时间去恢复,不过这个时间不是无限的。在切走之前先创建一个分支,能让分离状态下的工作安全无虞。今天从 reflog 里把某次提交找回来是可行的,但这条记录终究会过期,而且往往在过期之前,你就早已忘了那个提交的哈希值,所以要养成随手建分支的习惯,reflog 留给偶尔忘记的那几次少数情况用,具体做法在撤销操作这一节里讲。

JunoHEAD 和分离头指针 HEAD 标记着你当前所站的那次提交,通常它会跟着你所在的分支一起移动。如果你检出的是某个具体提交而不是某个分支,你就会进入分离头指针状态,在那里做的任何新提交都没有分支名指向它。要是遇到这种情况,又想保留这些工作,就得在切换到别处之前,当场创建一个分支。
JunoHEAD 和分离头指针 你最可能碰到分离头指针的场景,是检出某个标签或旧提交去查看代码,只要你只是在查看,就没什么问题。一旦你提交了什么值得保留的东西,就当场运行 git switch -c 给它建个分支,再去做别的事。
JunoHEAD 和分离头指针 通常情况下,HEAD 指向一个分支,分支再指向一次提交。而在分离状态下,HEAD 直接指向那次提交,所以你在那里新加的任何东西,一旦切走,就没有分支名再拴着它了。这种改动很少会真的永久丢失,因为 reflog 会留下痕迹,但你可别指望三周之后还能记得那个提交的哈希值。永远记得先建分支,再切换。

接下来该学什么 ​

到这里,你已经学会了创建分支、在分支之间切换,也见到了当你在别处做实验时,main 是怎么保持不动的。接下来的问题是,分支上的工作要怎么合并回 main,这正是合并与冲突要讲的内容。如果你想在往下学之前,先回顾一下如何查看提交历史,历史记录会带你过一遍 git log、git show 和 git diff。