一、基础篇
1.1 Git merge
我们准备了两个分支,每个分支上各有一个独有的提交。这意味着没有一个分支包含了我们修改的所有内容。咱们通过合并这两个分支来解决这个问题。
我们要把 bugFix 合并到 master 里
执行 git merge bugFix, 得到如下结果
哇哦!看见了吗?首先,master 现在指向了一个拥有两个父节点的提交记录。假如从 master 开始沿着箭头向上看,在到达起点的路上会经过所有的提交记录。这意味着 master 包含了对代码库的所有修改。↓↓↓
还有,看见各个提交记录的颜色变化了吗?为了帮助学习理解,我引入了颜色搭配。每个分支都有不同的颜色,而每个提交记录的颜色是所有包含该提交记录的分支的颜色混合之后的颜色。
所以,master 分支的颜色被混入到所有的提交记录,但 bugFix 没有。下面咱们让它也改变一下颜色。
咱们再把 master 分支合并到 bugFix:
git checkout bugFix
git merge master
因为 master 继承自 bugFix,Git 什么都不用做,只是简单地把 bugFix 移动到 master 所指向的那个提交记录。
现在所有提交记录的颜色都一样了,这表明每一个分支都包含了代码库的所有修改!大功告成!
1.2 Git rebase
第二种合并分支的方法是 git rebase。Rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。
Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。
咱们还是实际操作一下吧……
还是准备了两个分支;注意当前所在的分支是 bugFix(星号标识的是当前分支)
我们想要把 bugFix 分支里的工作直接移到 master 分支上。移动以后会使得两个分支的功能看起来像是按顺序开发,但实际上它们是并行开发的。
git rebase master
现在 bugFix 分支上的工作在 master 的最顶端,同时我们也得到了一个更线性的提交序列。
注意,提交记录 C3 依然存在(树上那个半透明的节点),而 C3' 是我们 Rebase 到 master 分支上的 C3 的副本。
现在唯一的问题就是 master 还没有更新,下面咱们就来更新它吧……
现在我们切换到 master 上。把它 rebase 到 bugFix 分支上……
git checkout master
git rebase bugFix
好了!由于 bugFix 继承自 master,所以 Git 只是简单的把 master 分支的引用向前移动了一下而已。
二、高级篇
2.1 分离HEAD
HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。
HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。
HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。
下面咱们通过实际操作看一下。我们将会观察提交前后 HEAD 的位置。
git checkout C2
如果想看 HEAD 指向,可以通过
cat .git/HEAD查看, 如果 HEAD 指向的是一个引用(即具体分支名而不是提交记录: C2),还可以用git symbolic-ref HEAD查看它的指向。
2.2 相对引用
通过git log可以查看commit hash并进行移动,但是还是显得麻烦。
使用相对引用的话,你就可以从一个易于记忆的地方(比如 bugFix 分支或 HEAD)开始计算。
相对引用非常给力,这里我介绍两个简单的用法:
- 使用
^向上移动 1 个提交记录 - 使用
~<num>向上移动多个提交记录,如~3
首先看看操作符 (^)。把这个符号加在引用名称的后面,表示让 Git 寻找指定提交记录的父提交。所以 master^ 相当于“master 的父节点”。master^^ 是 master 的第二个父节点。现在咱们切换到 master 的父节点
git checkout master^
// 等同于
git checkout master~1
2.3 强制修改分支位置
我使用相对引用最多的就是移动分支。可以直接使用 -f 选项让分支指向另一个提交。例如:
git branch -f master HEAD~3
上面的命令会将 master 分支强制指向 HEAD 的第 3 级父提交。
2.4 撤销变更
Git reset
git reset 通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset 向上移动分支,原来指向的提交记录就跟从来没有提交过一样。让我们来看看演示:
git reset HEAD^
值得注意的是 reset拥有多种用法:
| reset | reset --soft | reset --hard |
|---|---|---|
C2还在,处于未加入暂存区状态 | C2还在,处于暂存区状态 | C2不在了 |
Git revert
虽然在你的本地分支中使用 git reset 很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦!
为了撤销更改并分享给别人,我们需要使用 git revert。来看演示:
git revert HEAD
奇怪!在我们要撤销的提交记录后面居然多了一个新提交!这是因为新提交记录 C2' 引入了更改 —— 这些更改刚好是用来撤销 C2 这个提交的。也就是说 C2' 的状态与 C1 是相同的。
revert 之后就可以把你的更改推送到远程仓库与别人分享啦。
三、整理提交记录
如果我们想要改变提交记录的时间线,那么本节内容不可错过喔~
3.1 Git cherry-pick
如果你想将一些提交复制到当前所在的位置(HEAD)下面的话, Cherry-pick 是最直接的方式了。我个人非常喜欢 cherry-pick,因为它特别简单。
这里有一个仓库, 我们想将
side 分支上的工作复制到 master 分支,你立刻想到了之前学过的 rebase 了吧?但是咱们还是看看 cherry-pick 有什么本领吧。
git cherry-pick C2 C4
3.2 交互式 Rebase
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 —— 没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase —— 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了.
交互式 rebase 指的是使用带参数 --interactive 的 rebase 命令, 简写为 -i
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面(vim)并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
当 rebase UI界面打开时, 你能做3件事:
- 调整提交记录的顺序
- 删除你不想要的提交(通过切换
pick的状态来完成,关闭就意味着你不想要这个提交记录) - 合并提交。 它允许你把多个提交记录合并成一个。
这里大家可以动手实践一下,这块就不贴图了。
四、杂项
4.1 只取一个提交记录
本地栈式提交
来看一个在开发中经常会遇到的情况:我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。
这些调试和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因,解决掉以后觉得沾沾自喜!
最后就差把 bugFix 分支里的工作合并回 master 分支了。你可以选择通过 fast-forward 快速合并到 master 分支上,但这样的话 master 分支就会包含我这些调试语句了。你肯定不想这样,应该还有更好的方式……
实际我们只要让 Git 复制解决问题的那一个提交记录就可以了。跟之前我们在“整理提交记录”中学到的一样,我们可以使用
git rebase -igit cherry-pick
来达到目的。
执行下面命令结果如图:
git checkout master
git cherry-pick C4
执行下面命令结果如图:
git rebase -i HEAD~3
git branch -f master HEAD
4.2 提交的技巧
接下来这种情况也是很常见的:你之前在 newImage 分支上进行了一次提交,然后又基于它创建了 caption 分支,然后又提交了一次。
此时你想对的某个以前的提交记录进行一些小小的调整。比如设计师想修改一下 newImage 中图片的分辨率,尽管那个提交记录并不是最新的了。
我们可以通过下面的方法来克服困难:
- 先用
git rebase -i将提交重新排序,然后把我们想要修改的提交记录挪到最前 - 然后用
commit --amend来进行一些小修改 - 接着再用
git rebase -i来将他们调回原来的顺序 - 最后我们把 master 移到修改的最前端(用你自己喜欢的方法),就大功告成啦!
当然完成这个任务的方法不止上面提到的一种(我知道你在看 cherry-pick 啦),之后我们会多点关注这些技巧啦,但现在暂时只专注上面这种方法。 最后有必要说明一下目标状态中的那几个' —— 我们把这个提交移动了两次,每移动一次会产生一个 ';而 C2 上多出来的那个是我们在使用了 amend 参数提交时产生的,所以最终结果就是这样了。
git rebase -i HEAD~2 // 将C2 移到C3前面
git commit --amend -m "修改newImage commit 记录"
git rebase -i HEAD~2 // 将C2 移到C3后面
git branch -f master HEAD
上面的操作,唯一的问题就是要进行两次排序,而这有可能造成由 rebase 而导致的冲突。下面还是看看 git cherry-pick 是怎么做的吧。
要在心里牢记
cherry-pick可以将提交树上任何地方的提交记录取过来追加到HEAD上(只要不是HEAD上游的提交就没问题)。
git checkout master
git cherry-pick C2
git commit --amend -m "修改newImage commit 记录"
git cherry-pick C3
得到的结果如下,可以看出来,cherry-pick操作起来更加清晰,推荐优先使用git cherry-pick而不是git rebase -i <HEAD>
4.3 Git tag
相信通过前面课程的学习你已经发现了:分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。
你可能会问了:有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
当然有了!Git 的 tag 就是干这个用的啊,它们可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。
更难得的是,它们并不会随着新的提交而移动。你也不能检出到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
咱们先建立一个tag,指向提交记录 C1,表示这是我们 1.0 版本。
git tag v1 C1
很容易吧!我们将这个tag命名为 v1,并且明确地让它指向提交记录 C1
如果你不指定提交记录,Git 会用
HEAD所指向的位置。
4.4 Git describe
由于tag在代码库中起着“锚点”的作用,Git 还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),它就是 git describe!
Git Describe 能帮你在提交历史中移动了多次以后找到方向;当你用 git bisect(一个查找产生 Bug 的提交记录的指令)找到某个提交记录时,或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令。
git describe 的语法是:
git describe <ref>
<ref>可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会以你目前所检出的位置(HEAD)。
它输出的结果是这样的:<tag>_<numCommits>_g<hash>
tag表示的是离ref最近的标签,numCommits是表示这个ref与tag相差有多少个提交记录,hash表示的是你所给定的ref所表示的提交记录哈希值的前几位。
当 ref 提交记录上有某个标签时,则只输出标签名称,让我们来看一个例子
git describe master 会输出:v1_2_gC2
git describe side 会输出:v2_1_gC4
五、进阶篇
5.1 多分支Rebase
哥们儿,我们准备了很多分支!咱们把这些分支 rebase 到 master 上吧。
但是你的领导给你提了点要求 —— 他们希望得到有序的提交历史,也就是我们最终的结果应该是 C6' 在 C7' 上面, C5' 在 C6' 上面,依此类推。
git rebase master bugFix // 将bugFix rebase 到 master 下,同时切换到 master。以下操作同理。
git rebase bugFix side
git rebase side another
git rebase another master
5.2 两个父节点
操作符 ^ 与 ~ 符一样,后面也可以跟一个数字。
但是该操作符后面的数字与 ~ 后面的不同,并不是用来指定向上返回几代,而是指定合并提交记录的某个父提交。还记得前面提到过的一个合并提交有两个父提交吧,所以遇到这样的节点时该选择哪条路径就不是很清晰了。
Git 默认选择合并提交的“第一个”父提交,在操作符 ^ 后跟一个数字可以改变这一默认行为。
废话不多说,举个例子。
-
使用
git checkout master^后HEAD指向C1 -
使用
git checkout master^2后HEAD指向C2
5.3 纠缠不清的分支
现在我们的 master 分支是比 one、two 和 three 要多几个提交。出于某种原因,我们需要把 master 分支上最近的几次提交做不同的调整后,分别添加到各个的分支上。
one 需要重新排序并删除 C5,two 仅需要重排排序,而 three 只需要提交一次。
操作如下:
git checkout one
git cherry-pick C4 C3 C2
git checkout two
git cherry-pick C5 C4 C3 C2
git branch -f three C2
本文例图均来自
Learn Git