这一系列共有两部分:
最近在项目开发的过程中经常需要使用Git,但我对Git的理解除了使用过常规的add,commit,push等之外,其他的都不太了解,完全不足以支撑我在日常项目开发中的需求,尤其是branch的使用可以说是非常菜了。恰好这两天看到一个非常有意思的Github项目,learnGitBranching,这是一个可视化+交互式学习Git分支的网站,并且是以关卡的形式呈现,一关一关打怪升级,最关键的是还有中文版,简直不要太友好!
网址:https://learngitbranching.js.org/
Github主页:https://github.com/pcottle/learnGitBranching
下面我将从零开始闯关(默认读者和我一样,掌握git的基本操作,但对分支不甚熟悉),记录在此过程中遇到的问题以及笔记等,希望能学会branch的使用,并且能够给读者以帮助。先放一张主页图:
这一部分将主要介绍本地分支的相关操作,文章内的顺序将按照相关性进行组织和分类,可能与原始网站中的排布有些许的出入,但都是为了方便理解和记忆。
一. 基础篇
循序渐进地介绍 Git 主要命令
1. Git Commit
这是提交操作,也是最基本的操作。
知识点:
a. Git 仓库中的提交记录保存的是目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!
b. Git 希望提交记录尽可能地轻量,因此在每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。
c. Git 还保存了提交的历史记录,这也是为什么大多数提交记录的上面都有父节点的原因,对于项目组的成员来说,维护提交历史对大家都有好处。
可以把提交记录看作是项目的快照。提交记录非常轻量,可以快速地在这些提交记录之间切换!
2. Git Branch
Git 的分支也非常轻量。它们只是简单地指向某个提交纪录 —— 仅此而已。所以许多 Git 爱好者传颂:
早建分支!多用分支!
这是因为即使创建再多的分支也不会造成储存或内存上的开销,并且按逻辑分解工作到不同的分支要比维护那些特别臃肿的分支简单多了。
分支其实就相当于在说:“我想基于这个提交以及它所有的父提交进行新的工作。”
2.1 分支的创建和切换
创建分支和切换分支命令:
# 创建但不切换分支
git branch <new-branch-name>
# 切换到已有分支
git checkout <your-branch-name>
# 创建同时切换分支
git checkout -b <new-branch-name>
用learnGitBranching上的图示来说明,新建一个分支其实就相当于是多了一个“指针”,指向当前的提交记录,而不是完全复制一份一模一样的出去。如下图,这是在master分支上(星号标记)执行git branch newImage的结果。
2.2 分支的高级用法
在闯关的时候发现了一个在任何位置新建分支的方法:
# 在任意位置新建分支,这里的<pos>如果不指定的话,就默认是HEAD
git branch <branch-name> [<pos>]
同时,还有一种强制修改分支指向的方法:
# 强制分支指向某个提交记录
git branch -f <branch-name> <pos>
后面介绍完“相对引用”等知识后,就会对这两种用法有更深的理解。
3. Git Merge
merge操作是分支中一个重要操作,用来合并两个分支。常用于新建一个分支,在其上开发某个新功能,开发完成后再合并回主线这种情景。以learnGitBranching上的图示来说明。
- 首先,有两个分支,master和bugFix,分别基于C1提交各自修改了一部分内容。当前分支是master(星号表示)。
- 使用如下操作,在Git中会产生一个特殊的提交记录,它有两个父节点。可以理解为“要将这两个父节点本身及它们所有的祖先都包含进来”。此时master分支指向了一个拥有两个父节点的提交记录。
# 将<branch-name>与当前所在分支合并
git merge <branch-name>
示例中当前分支为master,<branch-name>为bugFix
- 如果要继续更新bugFix分支,则只需要切换到bugFix分支上,再执行
git merge master操作即可,此时因为bugFix已经是master分支的父节点了,因此再merge master分支的时候,就是直接将bugFix移动到当前master的提交记录上。

4. Git Rebase
这是另外一种合并分支的方法,我之前完全没有听说过。Rebase即是取出一系列的提交记录,然后“复制”它们,在另外一个地方逐个的放下去。它的优势是可以创造更线性的提交历史,如果仅使用Rebase的话,代码库的提交历史将会变得异常清晰。截取learnGitBranching上的图示来说明。
- 首先,还是有两个分支,master和bugFix,分别基于C1提交各自修改了一部分内容。当前分支是bugFix(星号表示)。
- 使用如下操作,会将bugFix分支里的工作直接移到master分支上,移动以后会使得两个分支的功能看起来更像是按照顺序开发,但实际上是并行开发。可以看出,在rebase之后,bugFix分支的工作移至了master分支的最顶端,同时也得到了一个更为线性的提交序列。这里要注意的是,之前的bugFix的提交记录C3依然还在(树上的半透明节点),C3’则是Rebase(可理解为复制)到master分支上的C3的副本。
# 将当前分支复制到<branch-name>上
git rebase <branch-name>
# 示例中当前分支为bugFix,<branch-name>为master
# 另一种合并用法
git rebase <target> <origin>
# 将<origin>节点的内容rebase到<target>上去,并切换到<origin>节点
- 此时master分支还没有被更新,要想继续更新master,只需要切换到master分支上,然后执行
git rebase bugFix即可,由于bugFix继承自master,所以这一步只是简单的把master分支的引用向前移动一下。

这里要注意Rebase与Merge在使用上的区别,merge xxx是将xxx分支合并到当前分支上来,而rebase xxx则是将当前分支复制到xxx分支上去。两者的方向是不一样的,但其实如果不考虑底层的话,在实际使用感上没有区别。
好了,基础的部分就到这里,下面将进入高级篇。
二. 高级篇
1. 分离HEAD
首先来看一下“HEAD”,这是一个对当前检出记录的符号引用,也即指向你正在其基础上进行工作的提交记录。
HEAD总是指向当前分支上最近一次的提交记录,大多数修改提交树的Git命令都是从改变HEAD的指向开始的。
HEAD通常情况下是指向分支名的(如之前的bugFix),在提交时,改变了bugFix的状态,这一变化通过HEAD变得可见。
如果想看HEAD的指向,可通过cat .git/HEAD来查看,如果HEAD指向的是一个引用,还可以通过git symbolic-ref HEAD来查看引用的指向。
如果想让HEAD从指向分支名变为指向提交记录,即分离HEAD,则可以通过使用git checkout 提交记录名来实现,所以git checkout命令其实是在操作HEAD节点,后面跟着的内容,即是让HEAD指向的内容,例如,如果是分支名,则HEAD指向分支名,如果是提交记录名,则HEAD指向提交记录。
具体如下图所示,左边的是一个正常提交树的样子,master分支为当前分支,此时HEAD指向master分支名,master指向C1,即HEAD -> master -> C1,其中C1是提交记录名(其实是哈希值,这里简化表示)。在进行git checkout C1的操作后,实现分离HEAD,即HEAD直接指向C1。
2. 相对引用
2.1 为什么要用到相对引用?
在上面提到分离HEAD时,有一个很重要的点即是要指定所谓的“提交记录名”,这个在图示中,可以用C1等这种简单的方式来表示,但实际使用中是用哈希值来进行提交的记录。
通过指定提交记录哈希值的方式在Git中移动不太方便,有两个原因:1)哈希值需要用git log来进行查看,2)哈希值一般非常长(基于SHA-1,共40位),比如fed2da64c0efc5293610bdd892f82a58e8cbc5d8。但值得欣慰的是,Git中对哈希的处理比较智能,只需要提供能够唯一标识提交记录的前几个字符即可。比如上面的那个例子可以仅输入fed2。
更方便的在Git提交记录中移动的方法是“相对引用”。使用相对引用的话,就可以从一个易于记忆的地方(比如bugFix分支或HEAD)开始计算。
这里有两种做法:
- 使用
^向上移动一个提交记录 - 使用
~<num>向上移动多个提交记录,如~3
2.2 相对引用^
首先看操作符 ^ ,将此操作符加在引用名称的后面,表示让Git寻找指定提交记录的父提交,如 master^ 即为"master 的父节点"。用git checkout master^命令即可切换到master的父节点(注意:这里仅是HEAD指向了master的父节点,master引用本身没有变化)。当然也可以用HEAD本身作为相对引用的参照,如使用git checkout HEAD^在搜索树上移动(注意:这里的HEAD可以是指向如master这种分支名的,也可以是分离之后指向提交记录的,在这两种情况下指向上述命令,都会造成HEAD的分离,即最终指向的都是提交记录)。
2.3 相对引用~
如果想在提交树中向上移动很多步的话,要敲很多的 ^ ,会非常麻烦。所以这里引入第二种操作符 ~ 。该操作符后面可以跟一个数字(可选,不跟数字时与 ^ 相同,向上移动一次),指定向上移动多少次。
操作符^和~一样,后面也可以跟一个数字。但是该操作符后面的数字与~后面的不同,并不是用来指定向上返回几代,而是指定合并提交记录的某个父提交。因为之前提到过一个合并会产生两个父提交,这时就不知道该选择哪条路径了。
Git在默认情况下默认会选择合并提交的“第一个”父提交,在操作符^后面跟一个数字可以改变这一默认行为。
例如上面这个例子,左边的HEAD指向的是master^,右边的HEAD指向的是master^2。
利用^和~可以在提交树上面快速移动,并且可以支持链式操作!例如git checkout HEAD~^2~2这种。
2.4 使用场景
一般使用相对引用最多的地方就是移动分支。可以直接使用 -f 选项让分支强制指向某个提交记录。例如:git branch -f master HEAD~3,会将master分支强制指向HEAD向上数第三个父提交。 也可以用git branch -f master bugFix,让master分支强制指向bugFix分支所指向的提交记录。
记住:git checkout是对HEAD进行操作,git branch是对分支进行操作
3. 撤销变更
在Git中撤销变更的方法有很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。主要有两种方法来撤销变更,一种是git reset,另一种是git revert。
3.1 Git Reset — 适用于本地
首先是git reset,它是通过将分支记录回退几个提交记录来实现撤销改动。可以认为是“改写历史”,原来指向的提交记录就跟从来没有提交过一样。例如执行git reset HEAD~1之后,原来HEAD指向的提交记录不存在了,但其变更还在,只不过处于未加入暂存区的状态,即把之前提交过的东西拿出来再考量考量。这种使用git reset的方式在本地分支中很方便,但是对于远程分支来说是无法共享的。
3.2 Git Revert — 适用于远端
为了撤销更改并分享给远端的其他人,需要使用git revert操作,revert的操作其实在提交记录里面看是向前的操作,并没有回滚,例如用git revert HEAD之后,如下图所示:
左边是revert之前的状态,右边是revert之后的状态,可见撤销记录C2时并没有像reset那样,回滚到C1,而是产生了一个新的提交记录C2’,它的状态与C1是相同的,在revert过程中可能出现冲突,需要解决冲突。此时进行提交时候,就可以将撤销操作推送到远程仓库中了。
注意:使用git reset <branch-name>时是回滚到<branch-name>这个版本,而使用git revert <branch-name>时是回滚到<branch-name>的前一个版本。注意用法上的区别。
看到这里其实已经涵盖了git分支90%的常用功能,基本也足够满足开发者的日常需求。而如果想处理更复杂的工作流时,或陷入困惑时,可以继续看后面的10%。
三. 自由修改提交树
这些技能能帮助你自由打造自己的提交记录树。
1. Git Cherry-pick
如果想要将一些提交复制到当前所在的位置(HEAD)下面的话,Cherry-pick是一种最直接的方式。其命令形式为:
git cherry-pick <提交号>...
如执行git cherry-pick C2 C4前后的示意图如下:
可见cherry-pick命令可以将所需的提交记录“抓过来”放到当前分支下。
注意,可以同时操作很多个提交记录,并且按照顺序依次抓过来,越靠后的越放在后面(父亲),并且一定是只抓一个节点,与rebase还不同,不会抓它们的父亲节点。
2. 交互式 rebase
当知道所需要的提交记录时(并且还知道这些提交记录的哈希值时),使用cherry-pick比较方便。但如果不清楚想要的提交记录的哈希值,就可以使用交互式的rebase,帮助你从一系列的提交记录中找到想要的记录。
交互式rebase其实指的就是带参数–interactive的rebase命令,简写为-i。执行此命令的时候,Git会打开一个UI界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有利于理解这个提交进行了哪些更改。
在实际使用时,所谓的UI窗口一般会在文本编辑器如Vim中打开一个文件。具体的操作方式可以看文件中的说明,这里仅放一下文件说明:
这里要注意的是:如果执行类似git rebase -i HEAD~4这种命令时,会默认将HEAD向上的4个父节点都默认进行pick,如下图所示:
这里的操作基本都是复制操作,不会真正删除提交记录
四. Tag 和 Describe
这里继续介绍两个功能:
1. Git Tag
从前面的一系列操作可以发现:分支很容易被人所移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。
那么有没有什么可以永远指向某个提交记录的标识呢?比如软件发布新的大版本,或者是修正一些重要的Bug,或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
Git中的tag就可以很好的解决这个问题,它可以永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。并且,它不会随着新的提交而移动,也不能检出到某个标签上面进行修改提交,它就像是提交树上的一个锚点,仅标识了某个特定的位置。
其使用命令如下:
# 将<提交记录>打上<tag-name>标签
git tag <tag-name> <提交记录>
打上tag之后的提交记录相当于是起了一个别名,后面可以用此别名指代这个提交记录。
2. Git Describe
由于标签在代码库中起着“锚点”的作用,Git还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),即git describe。
Git Describe能帮你在提交历史中移动了多次以后找到方向;当你用git bisect(一个二分法查找产生 Bug 的提交记录的指令,想到了之前刷Leetcode中经典题目"Bad Version")找到某个提交记录时,或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令。
其使用方式为:
git descirbe <ref>
<ref>可以是任何能被Git识别成提交记录的引用,若没有指定,则使用HEAD。其输出为:
<tag>_<numCommit>_g<hash>
其中<tag>表示的是离<ref>最近的标签,<numCommit>是表示这个<ref>与<tag>相差有多少个提交记录,<hash>表示的是<ref>所表示的提交记录哈希值的前几位。
当<ref>提交记录上有某个标签时,则只输出标签名称。
五. 场景分析
1. 只取一个提交记录
考虑这样一种情况,在主分支开发过程中,遇到了一个bug,因此新开了一个分支bugFix,专门处理这个bug,并且为了调试方便而在代码中添加了一些调试命令并向控制台打印了一些信息。这些调试和打印语句都在它们各自的提交记录里面,在修改完bug之后,需要与主分支合并,但却并不想要这些调试语句,这时就需要提取真正的bugFix分支中对解决bug有用的提交。
在之前已经学过如何对某个提交进行操作,无非是使用git rebase -i和git cherry-pick即可实现。
2. 提交的技巧
考虑到一种常见的情况,即你之前在newImage分支上进行了一次提交,然后又基于它创建了caption 分支,然后又提交了一次。
此时你想对的某个以前的提交记录进行一些小小的调整。比如设计师想修改一下newImage中图片的分辨率,尽管那个提交记录并不是最新的了。
这时可以使用如下的流程来进行此项操作:
- 先用
git rebase -i将提交重新排序,然后将我们想要修改的提交记录挪到最前面 - 然后用
git commit --amend来进行提交记录的修改(这一步将会产生一个新的与当前记录平级的提交记录) - 再用
git rebase -i将两次提交调回原来的顺序即可
上面的操作主要是基于git rebase -i进行操作,但这里需要两次排序,极有可能会造成由rebase而导致的冲突,有一种更好的做法是通过git cherry-pick,如下:
- 先用
git cherry-pick对newImage进行复制 - 利用
git commit -amend进行提交记录的修改 - 再用
git cherry-pick对caption进行复制即可
4万+





