图解Git
# 图解Git
之前已经学过git,并且我日常使用中很少借助可视化工具来提交代码。基本都是通过命令来操作,所以我认为git这块我没问题,但是来了公司了以后,发现自己大错特错,所以打算重新学习一下Git。
学习官网: https://learngitbranching.js.org/?locale=zh_CN
# Git Commit
Git 仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!
Git 希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。
Git 还保存了提交的历史记录。这也是为什么大多数提交记录的上面都有 parent 节点的原因 —— 我们会在图示中用箭头来表示这种关系。对于项目组的成员来说,维护提交历史对大家都有好处。
c2和c1都是新提交的节点
git commit -m "提交信息"
# Git Branch
Git 的分支也非常轻量。它们只是简单地指向某个提交纪录 —— 仅此而已。所以许多 Git 爱好者传颂:
早建分支!多用分支!
这是因为即使创建再多的分支也不会造成储存或内存上的开销,并且按逻辑分解工作到不同的分支要比维护那些特别臃肿的分支简单多了。
在将分支和提交记录结合起来后,我们会看到两者如何协作。现在只要记住使用分支其实就相当于在说:“我想基于这个提交以及它所有的 parent 提交进行新的工作。”
git branch 新分支名称 # 创建一个新的分支
git checkout 新分支名称 # 切换分支
git checkout -b 分支名称 # 创建并切换分支,是上面两个命令的简写
2
3
4
注意:在 Git 2.23 版本中,引入了一个名为
git switch
的新命令,最终会取代git checkout
,因为checkout
作为单个命令有点超载(它承载了很多独立的功能)。 由于现在很多人还无法使用switch
,本次课程仍然使用checkout
而不是switch
, 但是如果你想尝试一下新命令,我们的应用也是支持的!并且你可以从这里 (opens new window)学到更多关于新命令的内容。
# 分支与合并
太好了! 我们已经知道如何提交以及如何使用分支了。接下来咱们看看如何将两个分支合并到一起。就是说我们新建一个分支,在其上开发某个新功能,开发完成后再合并回主线。
咱们先来看一下第一种方法 —— git merge
。在 Git 中合并两个分支时会产生一个特殊的提交记录,它有两个 parent 节点。翻译成自然语言相当于:“我要把这两个 parent 节点本身及它们所有的祖先都包含进来。”
我们准备了两个分支,每个分支上各有一个独有的提交。这意味着没有一个分支包含了我们修改的所有内容。咱们通过合并这两个分支来解决这个问题。
我们要把 bugFix
合并到 main
里
我们准备了两个分支,每个分支上各有一个独有的提交。这意味着没有一个分支包含了我们修改的所有内容。咱们通过合并这两个分支来解决这个问题。
我们要把 bugFix
合并到 main
里
哇哦!看见了吗?首先,main
现在指向了一个拥有两个 parent 节点的提交记录。假如从 main
开始沿着箭头向上看,在到达起点的路上会经过所有的提交记录。这意味着 main
包含了对代码库的所有修改。↓↓↓
咱们再把 main
分支合并到 bugFix
:
git checkout bugFix
git merge main
2
命令练习
git checkout -b bugFix # 创建并切换到bugFix分支
git commit -m "" # 往bugFix分支上提交内容
git checkout main # 切换到main分支
git commit -m "" # 往main分支上提交信息
git merge bufFix # 在main分支上合并bugFix分支
2
3
4
5
# Git Rebase
第二种合并分支的方法是 git rebase
。Rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。
Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。
还是准备了两个分支;注意当前所在的分支是 bugFix(星号标识的是当前分支)
我们想要把 bugFix 分支里的工作直接移到 main 分支上。移动以后会使得两个分支的功能看起来像是按顺序开发,但实际上它们是并行开发的。
咱们这次用 git rebase
实现此目标
# 当前分支是bugFix
git rebase main
2
怎么样?!现在 bugFix 分支上的工作在 main 的最顶端,同时我们也得到了一个更线性的提交序列。
注意,提交记录 C3 依然存在(树上那个半透明的节点),而 C3' 是我们 Rebase 到 main 分支上的 C3 的副本。
现在唯一的问题就是 main 还没有更新,下面咱们就来更新它吧……
现在我们切换到了 main
上。把它 rebase 到 bugFix
分支上……
#当前分支main
git rebase bugFix
2
由于 bugFix
继承自 main
,所以 Git 只是简单的把 main
分支的引用向前移动了一下而已。
练习
git checkout -b bugFix # 创建并切换到bugFix分支
git commit -m "" # 往bugFix分支上提交内容
git checkout main # 切换到main分支
git commit -m "" # 往main分支上提交信息
#切换到bugFix分支下
git checkout bugFix
git rebase main
2
3
4
5
6
7
8
# 在提交树上移动
在接触 Git 更高级功能之前,我们有必要先学习在你项目的提交树上前后移动的几种方法。
一旦熟悉了如何在 Git 提交树上移动,你驾驭其它命令的能力也将水涨船高!
# HEAD
我们首先看一下 “HEAD”。 HEAD 是一个对当前所在分支的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。
HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。
HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。
使用 git checkout
命令并提供提交记录的哈希值来分离 HEAD
:
git checkout <commit_hash> # 具体提交记录的哈希值
# 相对引用
通过指定提交记录哈希值的方式在 Git 中移动不太方便。在实际应用时,并没有像本程序中这么漂亮的可视化提交树供你参考,所以你就不得不用 git log
来查查看提交记录的哈希值。
并且哈希值在真实的 Git 世界中也会更长(译者注:基于 SHA-1,共 40 位)。例如前一关的介绍中的提交记录的哈希值可能是 fed2da64c0efc5293610bdd892f82a58e8cbc5d8
。舌头都快打结了吧...
比较令人欣慰的是,Git 对哈希的处理很智能。你只需要提供能够唯一标识提交记录的前几个字符即可。因此我可以仅输入fed2
而不是上面的一长串字符。
正如我前面所说,通过哈希值指定提交记录很不方便,所以 Git 引入了相对引用。这个就很厉害了!
使用相对引用的话,你就可以从一个易于记忆的地方(比如 bugFix
分支或 HEAD
)开始计算。
相对引用非常给力,这里我介绍两个简单的用法:
- 使用
^
向上移动 1 个提交记录 - 使用
~<num>
向上移动多个提交记录,如~3
首先看看操作符 (^)。把这个符号加在引用名称的后面,表示让 Git 寻找指定提交记录的 parent 提交。
所以 main^
相当于“main
的 parent 节点”。
main^^
是 main
的第二个 parent 节点
现在咱们切换到 main 的 parent 节点
git checkout main^
git checkout bugFix^ # 移动到当前节点的父节点
# “~”操作符
如果你想在提交树中向上移动很多步的话,敲那么多 ^
貌似也挺烦人的,Git 当然也考虑到了这一点,于是又引入了操作符 ~
。
该操作符后面可以跟一个数字(可选,不跟数字时与 ^
相同,向上移动一次),指定向上移动多少次。咱们还是通过实际操作看一下吧
# 强制修改分支位置
我使用相对引用最多的就是移动分支。可以直接使用 -f
选项让分支指向另一个提交。例如:
git branch -f main HEAD~3
上面的命令会将 main 分支强制指向 HEAD 的第 3 级 parent 提交。
右图为原始图
git checkout HEAD~1 # 当前Head向父节点移动
git branch -f bugFix HEAD~1 # 移动bugFix分支到Head的父节点
git checkout c6 # 移动HEAD到content_hash为C6的位置
git branch -f main HEAD # 移动main分支到当前的HEAD节点的位置
git checkout HEAD~3 # 移动HEAD结点到当前结点的前三个节点
2
3
4
5
# 撤销变更
在 Git 里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。我们这个应用主要关注的是后者。
主要有两种方法用来撤销变更 —— 一是 git reset
,还有就是 git revert
。接下来咱们逐个进行讲解。
# Git Reset
git reset
通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset
向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
git reset HEAD~1
Git 把 main 分支移回到 C1
;现在我们的本地代码库根本就不知道有 C2
这个提交了。(在reset后, C2
所做的变更还在,但是处于未加入暂存区状态。)
# Git Revert
虽然在你的本地分支中使用 git reset
很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦!
为了撤销更改并分享给别人,我们需要使用 git revert
。
git revert HEAD
奇怪!在我们要撤销的提交记录后面居然多了一个新提交!这是因为新提交记录 C2'
引入了更改 —— 这些更改刚好是用来撤销 C2
这个提交的。也就是说 C2'
的状态与 C1
是相同的。
revert 之后就可以把你的更改推送到远程仓库与别人分享啦。
练习
分别撤销 local
分支和 pushed
分支上的最近一次提交。共需要撤销两个提交(每个分支一个)。
git reset main # 撤销local的最后一次提交
git checkout c2 # 切换到c2这个节点
git revert HEAD # 切换pushed分支到最近的一次提交
git branch -f pushed HEAD # 切换pushed分支到当前节点
2
3
4
# Git Cherry-pick
本系列的第一个命令是 git cherry-pick
, 命令形式为:
git cherry-pick <提交号>...
如果你想将一些提交复制到当前所在的位置(HEAD
)下面的话, Cherry-pick 是最直接的方式了。我个人非常喜欢 cherry-pick
,因为它特别简单。
这里有一个仓库, 我们想将 side
分支上的工作复制到 main
分支,你立刻想到了之前学过的 rebase
了吧?但是咱们还是看看 cherry-pick
有什么本领吧。
git cherry-pick C2 C4
练习
git cherry-pick c3 c4 c7
# 交互式的 rebase
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 —— 没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase —— 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了
交互式 rebase 指的是使用带参数 --interactive
的 rebase 命令, 简写为 -i
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
在实际使用时,所谓的 UI 窗口一般会在文本编辑器 —— 如 Vim —— 中打开一个文件。 考虑到课程的初衷,我弄了一个对话框来模拟这些操作。
当 rebase UI界面打开时, 你能做3件事:
- 调整提交记录的顺序(通过鼠标拖放来完成)
- 删除你不想要的提交(通过切换
pick
的状态来完成,关闭就意味着你不想要这个提交记录) - 合并提交。 遗憾的是由于某种逻辑的原因,我们的课程不支持此功能,因此我不会详细介绍这个操作。简而言之,它允许你把多个提交记录合并成一个。
练习:
git rebase -i HEAD~4
# 然后删除C2, 调整C4和C5的顺序
2
# 本地栈式提交
来看一个在开发中经常会遇到的情况:我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。
这些调试和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因,解决掉以后觉得沾沾自喜!
最后就差把 bugFix
分支里的工作合并回 main
分支了。你可以选择通过 fast-forward 快速合并到 main
分支上,但这样的话 main
分支就会包含我这些调试语句了。你肯定不想这样,应该还有更好的方式……
实际我们只要让 Git 复制解决问题的那一个提交记录就可以了。跟之前我们在“整理提交记录”中学到的一样,我们可以使用
git rebase -i
git cherry-pick
由于我们刚刚闯过类似的关卡,所以要不要再尝试一次就看你自己了。但是如果你想试一把的话,确保 main
分支能得到 bugFix
分支上的相关提交。
# 可以看到图中当前分支是bugFix
git checkout main # 切换到main分支
git cherry-pick c4
2
3
# 第二种实现方案
git rebase -i HEAD~3
git branch -f main bugFix # 修改main分支位置到bugFix结点
2
3
# 提交的技巧 #1
接下来这种情况也是很常见的:你之前在 newImage
分支上进行了一次提交,然后又基于它创建了 caption
分支,然后又提交了一次。
此时你想对某个以前的提交记录进行一些小小的调整。比如设计师想修改一下 newImage
中图片的分辨率,尽管那个提交记录并不是最新的了。
我们可以通过下面的方法来克服困难:
- 先用
git rebase -i
将提交重新排序,然后把我们想要修改的提交记录挪到最前 - 然后用
git commit --amend
来进行一些小修改 - 接着再用
git rebase -i
来将他们调回原来的顺序 - 最后我们把 main 移到修改的最前端(用你自己喜欢的方法),就大功告成啦!
当然完成这个任务的方法不止上面提到的一种(我知道你在看 cherry-pick 啦),之后我们会多点关注这些技巧啦,但现在暂时只专注上面这种方法。 最后有必要说明一下目标状态中的那几个'
—— 我们把这个提交移动了两次,每移动一次会产生一个 '
;而 C2 上多出来的那个是我们在使用了 amend 参数提交时产生的,所以最终结果就是这样了。
也就是说,我在对比结果的时候只会对比提交树的结构,对于 '
的数量上的不同,并不纳入对比范围内。只要你的 main
分支结构与目标结构相同,我就算你通过。
git rebase -i HEAD~2
git rebase -i HEAD~1
git rebase -i HEAD~2
git branch -f main caption
# 上面也可以实现,下面是答案
git rebase -i HEAD~2 # 调整C2和C3位置
git commit --amend
git rebase -i HEAD~2 # 调整C2和C3位置
git branch -f main
2
3
4
5
6
7
8
9
10
# 提交的技巧 #2
如果你还没有完成“提交的技巧 #1”(前一关)的话,请先通过以后再来!
正如你在上一关所见到的,我们可以使用 rebase -i
对提交记录进行重新排序。只要把我们想要的提交记录挪到最前端,我们就可以很轻松的用 --amend
修改它,然后把它们重新排成我们想要的顺序。
但这样做就唯一的问题就是要进行两次排序,而这有可能造成由 rebase 而导致的冲突。下面还是看看 git cherry-pick
是怎么做的吧。
要在心里牢记 cherry-pick 可以将提交树上任何地方的提交记录取过来追加到 HEAD 上(只要不是 HEAD 上游的提交就没问题)。
练习
git checkout main
git cherry-pick c2
git branch -f main c1
git cherry-pick c2' c3
# 答案应该这样来
git checkout main
git cherry-pick newImage
git commit --amend
git cherry-pick caption
2
3
4
5
6
7
8
9
10
# Git Tags
相信通过前面课程的学习你已经发现了:分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。
你可能会问了:有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
当然有了!Git 的 tag 就是干这个用的啊,它们可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。
更难得的是,它们并不会随着新的提交而移动。你也不能切换到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
咱们来看看标签到底是什么样。
git checkout c1
git tag v0
git checkout c2
git tag v1
# 答案
git tag v0 c1
git tag v1 c2
git checkout c2
2
3
4
5
6
7
8
9
# Git Describe
由于标签在代码库中起着“锚点”的作用,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
提交记录上有某个标签时,则只输出标签名称
# 多分支 rebase
哇,现在我们这里出现了很多分支呢!让我们把所有这些分支上所做的工作都通过 rebase 合并到 main 分支上吧。
但是你的领导给你提了点要求 —— 他们希望得到有序的提交历史,也就是我们最终的结果应该是 C6'
在 C7'
上面, C5'
在 C6'
上面,依此类推。
即使你搞砸了也没关系,用 reset
命令就可以重新开始了。记得看看我们提供的答案,看你能否使用更少的命令来完成任务!
git checkout bugFix # 切换到bugFix分支
git rebase main
git branch -f main # 更新main分支到最新的节点
git checkout side
git rebase main
git branch -f main
git checkout another
git rebase main
git branch -f main
# 下面是答案
git rebase main bugFix # 当前在main分支
git rebase bugFix side # 当前在bugFix分支
git rebase side another # 当前在side分支
git branch -f main another
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 选择 parent 提交记录
操作符 ^
与 ~
符一样,后面也可以跟一个数字。
但是该操作符后面的数字与 ~
后面的不同,并不是用来指定向上返回几代,而是指定合并提交记录的某个 parent 提交。还记得前面提到过的一个合并提交有两个 parent 提交吧,所以遇到这样的节点时该选择哪条路径就不是很清晰了。
Git 默认选择合并提交的“第一个” parent 提交,在操作符 ^
后跟一个数字可以改变这一默认行为。
git checkout main^
git checkout main^2
git checkout HEAD~ # 向父节点移动一个,从C7移动到C6
git checkout HEAD^2 # 遇到两个父节点,选择第二个父节点 从C6移动到C5
git chekout HEAD~2 # 从C5移动到C3
#简写
git checkout HEAD~^2~2
2
3
4
5
6
练习
git checkout HEAD~^2~ # HEAD节点从C7移动到了C2
git branch bugWork # 在C2创建bugWork分支
git checkout main # 切换HEAD指向main
#答案
git branch bugWork HEAD~^2~
2
3
4
5
6
# 纠缠不清的分支
哇塞大神!这关我们要来点不同的!
现在我们的 main
分支是比 one
、two
和 three
要多几个提交。出于某种原因,我们需要把 main
分支上最近的几次提交做不同的调整后,分别添加到各个的分支上。
one
需要重新排序并删除 C5
,two
仅需要重排排序,而 three
只需要提交一次。
慢慢来,你会找到答案的 —— 记得通关之后用 show solution
看看我们的答案哦。
练习
git checkout one # 切换到one分支
git cherry-pick c4 c3 c2
git checkout two # 切换到two分支
git cherry-pick c5 c4' c3' c2'
git checkout three
git branch -f three c2 # three分支指向C2结点
# 答案
git checkout one
git cherry-pick c4 c3 c2
git checkout two
git cherry-pick c5 c4 c3 c2
git branch -f three c2
2
3
4
5
6
7
8
9
10
11
12
13