Featured image of post (也许是)一个 Git 教程?其二

(也许是)一个 Git 教程?其二

看看 Git 分支(branch)吧!

上一节已经介绍了平时会怎么用 Git 进行单分支仓库的管理,这一节就来讲讲 Git 要怎么进行多分支协作吧!

头图信息请参考上一节内容,谢谢~ 选曲选到了知名社团 Foxtail-Grass-Studio 对小伞个人曲的翻调,请欣赏~

分支,是那个分支吗?

我们上一节已经了解过 Git 在单分支下的日常工作流了。值得注意的是,我们说的是“单分支”,那么自然,Git 是支持,同时鼓励使用多分支的。那么分支是什么呢?

也许有过 Galgame 经验,或者玩过有分支剧情游戏的你已经想到所谓的“分支”是什么东西了。没错,很像这么回事儿,不过功能更丰富一些,因为你不止是体验若干个分支的剧情,Git 甚至可以允许你在没有冲突的前提下合并两个分支!如果有一款游戏支持用 Git 来操控分支的话,也许就可以手动后宫了……

咳咳,不开玩笑了。我们来看看分支具体是什么样的。先来个分支图:

一个也许简单的 Git 分支示意图

gitGraph commit id: "initial commit" commit branch feature1 checkout feature1 commit id: "new feat1, first commit" commit checkout main merge feature1 id: "merge feature1" branch feature2 checkout feature2 commit id: "new feat2" checkout main commit merge feature2 id: "finish, merge feat2"
Git 分支示意图

(嘶,mermaid 竟然直接有 gitGraph 的功能,NB)

那么可以看到,我们这里有三条分支:一条 main,一条 feature1 以及一条 feature2。有时我们开启了一个分支,有时我们又将两个分支进行了合并。上面的图是怎么生成的呢(双关意)?下面是用到的代码:

 1gitGraph
 2    commit id: "initial commit"
 3    commit
 4    branch feature1
 5    checkout feature1
 6    commit id: "new feat1, first commit"
 7    commit
 8    checkout main
 9    merge feature1 id: "merge feature1"
10    branch feature2
11    checkout feature2
12    commit id: "new feat2"
13    checkout main
14    commit
15    merge feature2 id: "finish, merge feat2"

Mermaid 的 gitGraph 很有趣的地方在于,上面的代码几乎就是为了实现这样的提交树/分支形状所需要的 Git 命令。我们可以不管 id 后面的部分,因为这些在实际 commit 的时候应该是用 -m 来指定的提交信息才对。

那么,这些命令都干嘛了?要怎么用命令来操控分支?

和分支相关的命令们

下面来讲讲上面出现的(和没出现的一些)命令吧~

  • git checkout

    说实在的,这个命令真的是个很大的坑。git checkout 从 Git 诞生之初就已经存在,它是集创建、管理、变更分支或提交等功能为一体的一个命令。造成这个情况的主要原因在于 git checkout 实际上不是在我们现有的对 Git 存储模型的理解上进行操作,而是在 Git 更贴近实现层面的操作,即移动“指针”。

    然而,我们这里先不打算介绍这么深入/详细。我们还是从实用角度来聊聊这个命令。观察上面的 Mermaid 图,我们可以看到,好像 git checkout 的功能没有直接体现在图上。然而仔细观察的话可以猜到,git checkout 在这里的作用是更换分支。比如,git checkout main 就是告诉 Git“现在我要切换分支到 main 分支上”。这是 git checkout 的主要用途之一。另外我们还可以用 git checkout - 来像 cd - 一样切换到上一个分支。

    我们还可以对这个命令多讲一些。如果给它带上 -b 的参数则可以用来创建一个新分支。比如 git checkout -b new-branch 就可以创建一个新的名为 new-branch 的分支,同时你还会直接切换到该分支上。而如果你在后面带的参数是某个文件或者单纯的 .,则是要让 Git 该文件/所有文件里没有暂存的更改。

    上面说的都是比较老派的做法。相信你也一定从上面的 Mermaid 图中猜到了新式的创建新分支的方法,那就是:

  • git branch

    这个命令是用来管控和单个分支相关的操作的。我们简要介绍一下。

    如果后面不带任何的参数,则是会打印出可用分支。如果要创建一个新的分支,就可以用 git branch <another-branch>, 就是让 Git 尝试创建一个名为 <another-branch> 的分支。当这个分支已经存在的时候,Git 就会报错,告诉你已经有了叫这个名字的分支了。

    要注意的是,git branch <branch-name> 只会创建分支,并不会把当前分支更改到这个新分支上。要想在创建分支后切换分支,除了传统方式 git checkout 外,还可以使用更现代(?)的命令:git switch -c <branch-name>。我们后面会介绍到。

    除了创建分支以外,我们肯定还希望能实现查看/删除/重命名分支。我们干脆都列在下面吧。如果不想看可以跳过这一段。

    • 要查看分支,可以直接 git branch。如果要看所有的分支(包括远程的),可以使用 git branch -a 来查看。你还可以使用 -v 来输出上次提交的信息。
    • 要创建分支,就像上面说的,在后面补上你要的分支名称,即 git branch <branch-name>。如果这个分支已经存在则会报错,另外这个命令只会创建,并不会切换过去。
    • 如若要从某个提交上创建分支,还可以在 <branch-name> 后面添加上 <commit-hash>。至于 <commit-hash> 是什么,我们在后面关于 Git 的一些概念里进行介绍。
    • 想要删除分支,可以用 git branch -d <branch-name> 来删掉它。要是你要删除当前分支,请先切换到别的分支哦。
    • 要是打算重命名分支,可以考虑像操作文件一样 移动 它:git branch -m <branch-name> <new-name>。依旧,这个命令也只能更改别的分支。

    So, that’s it! Git 针对单分支的操作都可以用 branch 子命令来做到。那么,我们要怎么切换分支呢?除了 checkout 以外,“比较现代”(存疑)的方法是使用:

  • git switch

    这个命令是相对较新的用来切换分支的命令。可以通过 git switch <branch-name> 来简单地实现切换。有趣的是,我们还可以用 git switch -c <branch-name> 来创建新分支的同时切换过去。也就是说,git switch -c 命令和 git checkout -b 几乎是等价的。另外我们可以使用 git switch - 来直接跳回上一个分支。

    另外还可以考虑使用 git switch -m <branch-name> 来在切换分支的同时把当前分支合并到要切换的分支上。这一点还是相当不错的,因为我们经常会遇到这样的情形:在 dev 分支上完成某个特性之后,经过测试希望能合并到 main 分支上。如果没有这条命令的话,我们可能需要先 git checkout main 之后再 git merge dev,而有了这条命令我们就可以简单地 git switch -m main 了。

    总之,如果你需要切换分支,你就可以使用 switch 这个命令。语义很明确,不是吗?

  • git merge

    这个命令,如它的名字一样,是用来合并分支的,或者,不那么明显地,合并到当前分支。它的使用方式相对而言比较简单,就是单纯的 git merge <branch-name>

    这个命令的主要问题是,合并过程中会出现恶魔般的 冲突。解决冲突实在是一件令人头痛的事情(在我看来)。为了避免(逃避)合并冲突后的麻烦,你可以考虑 --abort 参数来告诉 Git 如果合并失败就什么都别动。然而,要是你真想合并,到底还是要解决冲突的。

    其实解决冲突就是一个“选择应用谁的代码”的过程。Git 会在发生冲突的地方用箭头标出来本分支和被合并分支的内容,你要做的就是把你不要的那个部分删掉然后保存。另外,合并会创建一个新的提交。如果你不喜欢默认提交信息,可以考虑使用 -e 参数来告诉 Git 你打算自己编辑合并产生的提交的提交信息。

    最后就是 Git 合并时有不同的策略。我们这里不多介绍,大部分情况可以使用 ff 模式,即 Fast Forward 模式。这个模式会让你的提交树看起来是一条直线,即如果历史提交相同的话就让两个分支有同样的提交了。

  • git log

    这个命令正如它名字所说的,会输出 Git 的分支记录。假如不跟任何的参数的话,它会简单地打印当前分支的提交记录们,信息包括提交的 SHA1 哈希结果,提交的作者/邮箱,提交日期,以及提交的信息。此时,Git 会进入自己的分页器方便你上下滚动浏览,支持 Vim 式的操作,比如 jk 翻页,/? 查询等等。自然,退出这个状态则需要按下 q

    这个命令当然没有这么枯燥。事实上,你可以自定义的部分特别多。例如,你可以使用 --graph 来在每个提交的最左边显示分支图(虽然不是很好辨认),你还可以使用 --all 来显示所有分支的历史记录。如果想让历史记录不要搞个好多行,而是只想看看每个提交的大概信息的话,你可以使用 --oneline 来让每个提交都变成短短的一行。上面说的这三个参数你可以组合在一起,来快速浏览提交历史是什么样的。而如果你希望显示全面的信息,比如哪些文件发生了什么更改,你可以使用 --stat ,这样 Git 就会有个统计信息,告诉你哪些文件发生了什么变化。

    你看到了提交的时间了吧?git log --before <date> --after <date> 还可以让你选定只查看某个时间段内的提交!时间的格式则是 yyyy-mm-dd,实在是非常方便的功能。

    然而这个命令最神奇的地方在于,你实际上可以自定义输出格式。使用 --pretty 格式,你就可以用一些字段来控制 Git 日志的输出格式。这里有一个参考表,感兴趣的话可以看看,试用一下。

分支相关的基本命令我们就先介绍到这里吧。有了上面的介绍,相信你已经可以运用 Git 的分支功能了吧~

Git 的概念们

然而,止步于介绍使用 Git 的方式,总是觉得不够透彻。知其然还要知其所以然,我们既然是在介绍 Git,那就尝试把 Git 更深一些(其实也没那么深)的概念多介绍一些吧。

仓库 (Repository)

我们几乎所有的 Git 项目都是从建立或者克隆 Git 仓库开始的。仓库是一个比较大的概念,我们和 Git 相关的所有内容都是要从仓库出发的,所有的信息都会存储在仓库中。

那么“所有信息”都有什么呢?这个问题会比较深,我们从表观的理解来讲,首先肯定得有我们工作内容息息相关的内容,毕竟 Git 就是用来管理它们的。另外就是和 Git 相关的内容了,大部分都存储在 .git 文件夹中,还有一些零散的 .gitignore 文件。其中 .git 存储了这个仓库的所有和 Git 直接相关的内容,例如文件快照,提交记录,不同的分支记录等等,都会以特殊的结构记录下来。这也意味着,如果你删了 .git 文件夹,那么这个仓库就没了,Git 的记录就全都消失啦。删除之前要好好想清楚咯~

然后 .gitignore 也是能控制 Git 行为的文件。它能够让 Git 不记录某些文件。比如说你有一些测试文件,它们其实不应该被记录在仓库里,只希望在本地有一份方便测试而已,那么就可以把他们的名字或者所在文件夹写进 .gitignore 里。

总之,Git 仓库就是这么个总的玩意儿了。有时我们会简称仓库英文为 repo,我还挺喜欢这个名字。

工作目录 (Working directory / Working tree)

这实际上就是我们正在编辑的项目目录。比如说我们从网上克隆了一个仓库之后,我们会进入这个仓库的目录里。这个仓库的根目录就是所谓的工作目录了。至于为什么叫“工作树”,我个人看法是因为 Git 分支的存在让整个仓库像树一样伸展开,或者是说目录下的文件层级结构像树一样吧。不过怎么想都觉得有点怪,毕竟如果是说仓库分支的话,我们应该是在树叶上而不是在树上吧……

然而,不深究的话,我们干活儿的地方就是工作目录。就是这样。

暂存区(Staging Area)

其实我们应该已经介绍过暂存区了。就像它的名字一样,暂存区是用来暂时存下“觉得改的差不多了”的内容的地方。我们用 git add 命令来把修改好的内容放在暂存区内等待提交。如果感觉暂存区的内容有不妥的地方,我们可以随时打回来重新修改。我们也可以把一些内容从暂存区撤下来。总之,暂存区给了我们再次考虑的机会。而假如我们认为“暂存区的东西我很满意,可以提交了”,我们就可以用 git commit 来提交 暂存区 的内容到分支上(或者仓库,取决于你怎么看这个行为)。

总之,暂存区就是一个介于“保存文件”和“保存整个工作目录状态”之间的一个地方。这也决定了 Git 的工作流是 修改文件 -> 保存文件 -> 交给暂存区 -> 提交至分支/仓库

分支 (Branch)

相信你已经对分支有所了解了。我们在创建仓库的同时,会创建一个主分支,曾经主分支名称为 master,后来因为一些政治原因,现在更多叫 main 了。除了主分支外,我们还可以有很多别的分支。这些分支允许我们在仓库里存储不同的信息,不同分支间不会产生干扰,而在我们希望的时候我们又可以对分支们做出诸如合并、删除等的改动。

分支就像平行世界一样,我们可以让两个分支拥有同样的过去,在某个地方发生变化,最后独立演化下去。而分支胜过平行世界的地方在于,我们可以在没有直接分歧的情况下把两个分支合并在一起,而不会出现“我才是蜘蛛侠”的问题。

分支可以说是 Git 的灵魂和精髓了。推荐多运用分支进行项目管理,相当好用。遇事不决开个分支先测试一下,这不失为一个好办法。

提交 (Commit)

我们有了一个分支之后我们就需要向这个分支不断做出提交了。每一次的提交都会让这个分支的记录变多一些,分支实际上也是记录的每一次的提交。大白话讲,提交就是存档,只不过这些存档要依附在某个世界线(分支)上而已。

提交可以说是组成分支的部分。当我们查看分支具体有什么的时候,映入我们眼帘的就是每一次的提交记录。所谓的合并分支,也不过是比较两个分支之间的提交情况,如果没有冲突的提交就可以顺利合并了。

要注意的是,在 Git 里我们不提交文件本身,我们提交的是文件的变更。也正是由于 变更 这一关键特征,让 Git 可以高效地进行版本控制,不过坏处也有,那就是面对二进制文件就显得有点笨笨的了:二进制文件可以认为是一变全变的,不像文本那样可以有明显的局部改动。这也说明我们应该尽量让 Git 记录纯文本的文件而非二进制文件。

另外,需要再提醒的是,提交只会提交暂存区内的内容。如果有改动发生但没有放在暂存区里的话,提交是不会搭理这些改动的。这一点还请注意。

远程(Remote)

虽然我们还没有介绍太多和远程仓库/托管平台的内容,但远程仓库确实是在 Git 设计之初就已经有了的关键概念了。

我们介绍过,Git 一开始的设计目的是所谓 分布式 版本管理系统。这个 分布式 就在于每个人都可以拥有一份源代码,然后大家可以互相传递自己的修改,也可以自由选择是否进行合并别人的修改。这样去中心化的特点是相当超前的设计。而为了实现这样的设想,我们必须让 Git 拥有连接到别人仓库的能力。远程也正是这么个东西。

Git 可以把网络上的仓库作为自己的远程库来使用。我们通常不直接和远程库中的文件交互,而是把提交作为基础单元和远程库进行交互。当我们有了新的提交或者新的分支时,我们就可以把本地的这些改动 推送 (push) 到远程仓库;当远程仓库有了新的变动时,我们可以把新的变动 拉取 (pull) 到本地来。我们会在下一节对 Git 的远程功能进行更详细的介绍。

总之,Git 的远程仓库让一份代码可以被保存在多个位置,并且让我们和这些位置的仓库进行交互,这样就能让我们和别人进行协作了。然而,由于现实协作的众多需求,最终 Git 还是发展出了很多代码托管平台,来方便大家存储 Git 的远程库,并让大家在远程库上进行协作,避免直接塞给别人电脑上。

后记

我必须立刻承认我这篇文章离不开 tldr,准确来说是 tealdeer 的帮助。很难想象没有 tldr 我要怎么介绍可用命令。唉,我还是对 Git 不够熟悉。如果里面有任何的错漏,又或是对这个系列有什么建议,请直接告诉我,谢谢,我会及时修改的(球球了,告诉我哪里写的不好吧,呜呜呜)。

另外我还想推荐一个很不错的网站,Learn Git Branching,一个让你在实际操作中练习 Git 分支管理的网页,从进行提交,创建分支,合并分支,到变基 (Rebase),远程库协作等复杂操作,全都有涉猎。我花了一下午通关,收获很大,因此墙裂建议。

下一节就是我们的最后一节内容,我打算聊聊 Git 的远程协作功能,以及协作时的注意事项等等。另外,由于深感 Git 命令之繁杂,我有计划做一个小工具来通过问答的方式给出合适的 Git 命令。我暂时将这个工具命名为 Giao,希望不会难产吧,哈哈。有兴趣的话也可以关注我/给我提建议,谢谢啦。

Licensed under CC BY-NC-SA 4.0
最后更新于 6月 26, 2026 15:20 UTC