网址:https://learngitbranching.js.org/
Git仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!
Git希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。
Git 还保存了提交的历史记录。这也是为什么大多数提交记录的上面都有父节点的原因一我们会在图示中用箭头来表示这种关系。对于项目组的成员来说,维护提交历史对大家都有好处。
上面展示了一个(小型)Git代码库。当前有两个提交记录----初始提交c0
和其后可能包含某些有用修改的提交c1
,
我们刚刚修改了代码库,并把这些修改保存成了一个提交记录c2
,c2
的父节点是c1
,父节点是当前提交中变更的基础。
Git 的分支也非常轻量。它们只是简单地指向某个提交纪录一仅此而已。所以许多Git 爱好者传颂:
早建分支!多用分支!
这是因为即使创建再多的分支也不会造成储存或内存上的开销,并且按逻辑分解工作到不同的分支要比维护那些特别臃肿的分支简单多了。
在将分支和提交记录结合起来后,我们会看到两者如何协作。现在只要记住使用分支其实就相当于在说:“我想基于这个提交以及它所有的父提交进行新的工作。”
git branch newImage之后就创建了分支,新分支指向的就是c1
。
往新分支里面提交一些东西,发现mian分支前进了,而neewImage分支还在原地,这是因为我们没有“在”这个新分支上,main右上角的*表示当前所在分支。
git checkout newImage
git commit
切换分支然后进行提交,看到新分支前进了。
创建并切换分支
git checkout -b
我们已经知道如何提交以及如何使用分支了。接下来咱们看看如何将两个分支合并到一起。就是说我们新建一个分支,在其上开发某个新功能,开发完成后再合并回主线。
咱们先来看一下第一种方法一git merge
。在 Git 中合并两个分支时会产生一个特殊的提交记录,它有两个父节点。翻译成自然语言相当于:“我要把这两个父节点本身及它们所有的祖先都包含进来。”
我们准备了两个分支,每个分支上各有一个独有的提交。这意味着没有一个分支包含了我们修改的所有内容。咱们通过合并这两个分支来解决这个问题。
我们要把bugFix
合并到main
里。
git merge bugFix
首先,main现在指向了一个拥有两个父节点的提交记录c4
。假如从main开始沿着箭头向上看,在到达起点的路上会经过所有的提交记录。这意味着main
包含了对代码库的所有修改。
每个分支都有不同的颜色,而每个提交记录的颜色是所有包含该提交记录的分支的颜色混合之后的颜色。所以,main
分支的颜色被混入到所有的提交记录,但bugFix
没有。下面咱们让它也改变一下颜色。
git checkout bugFix
git merge main
把main分支合并到bugFix中
因为main
继承自 bugFix
,Git 什么都不用做,只是简单地把 bugFix
移动到 main
所指向的那个提交记录。
现在所有提交记录的颜色都一样了,这表明每一个分支都包含了代码库的所有修改!
第二种合并分支的方法是git rebase 。Rebase实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。
Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase的话,代码库的提交历史将会变得异常清晰。
准备了两个分支;注意当前所在的分支是bugFix(星号标识的是当前分支)
我们想要把bugFix分支里的工作直接移到main分支上。移动以后会使得两个分支的功能看起来像是按顺序开发,但实际上它们是并行开发的。
git rebase main
当前是在bugFix分支,将main分支rebase相当于将bugFix分支复制一份放在main之后
切换到mian分支,把它rebase到bugFix分支上
git checkout main
git rebase bugFix
由于bugFix继承自main,所以git只是简单的把main分支的引用向前移动一下而已。
我们首先看一下"HEAD”。HEAD是一个对当前检出记录的符号引用----也就是指向你正在其基础上进行工作的提交记录。
HEAD总是指向当前分支上最近一次提交记录。大多数修改提交树的Git命令都是从改变HEAD 的指向开始的。
HEAD通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix的状态,这一变化通过HEAD 变得可见。
git checkout C1
git checkout main
git commit
git checkout c2
可以看到。HEAD指向了c1、main、c2,随着提交向前移动。
分离的 HEAD
分离的 HEAD就是让其指向了某个具体的提交记录而不是分支名。在命令执行之前的状态如下所示:
HEAD -> main ->C1
HEAD指向 main, main 指向C1
git checkout C1
执行完之后变成HEAD ->C1
通过指定提交记录哈希值的方式在Git 中移动不太方便。在实际应用时,并没有像本程序中这么漂亮的可视化提交树供你参考,所以你就不得不用 git log来查查看提交记录的哈希值。
通过哈希值指定提交记录很不方便,所以 git 引入了相对引用。
使用相对引用的话,你就可以从一个易于记忆的地方(比如bugFix分支或
HEAD)开始计算。
相对引用非常给力,这里介绍两个简单的用法:
^
向上移动1个提交记录~
向上移动多个提交记录,如~3
首先看看操作符(^
)。把这个符号加在引用名称的后面,表示让Git寻找指定提交记录的父提交。
所以main^
相当于main
的父节点,main^^
是main
的第二个父节点。
现在咱们切换到 main的父节点
也可以将 HEAD
作为相对引用的参照。下面咱们就用 HEAD 在提交树中向上移动几次。
git checkout c3
git checkoutHEAD^
git checkout HEAD^
git checkout HEAD^
如果你想在提交树中向上移动很多步的话,敲那么多^
貌似也挺烦人的,Git当然也考虑到了这一点,于是又引入了操作符~
。
该操作符后面可以跟一个数字(可选,不跟数字时与^
相同,向上移动一次),指定向上移动多少次。
咱们使用~
一次后退四次。
git checkout HEAD~4
强制修改分支位置
我们使用相对引用最多的就是移动分支。可以直接使用-f
选项让分支指向另一个提交。例如:
git branch -f main HEAD~3
上面的命令会将 main分支强制指向HEAD的第3级父提交。
git branch -f main HEAD~3
例子
在Git里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。我们这个应用主要关注的是后者。
主要有两种方法用来撤销变更-----一是git reset,还有就是
git revert。接下来咱们逐个进行讲解。
git reset通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset 向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
git reset HEAD~1
git把main分支移动到了c1,现在我们的本地代码库根本不知道有c2这个提交。
虽然在你的本地分支中使用git reset
很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦!
为了撤销更改并分享给别人,我们需要使用git revert
。
git revert HEAD
奇怪!在我们要撤销的提交记录后面居然多了一个新提交!这是因为新提交记录c2'
引入了更改-----这些更改刚好是用来撤销c2
这个提交的。也就是说c2
的状态与c1
是相同的。
revert 之后就可以把你的更改推送到远程仓库与别人分享啦。
到现在我们已经学习了Git 的基础知识―提交、分支以及在提交树上移动。这些概念涵盖了Git 90%的功能,同样也足够满足开发者的日常需求。
然而,剩余的 10%在处理复杂的工作流时(或者当你陷入困惑时)可能就显得尤为重要了。接下来要讨论的这个话题是“整理提交记录”―开发人员有时会说“我想要把这个提交放到这里,那个提交放到刚才那个提交的后面”,而接下来就讲的就是它的实现方式,非常清晰、灵活,还很生动。
本系列的第一个命令是git cherry-pick
,命令形式为:git cherry-pick<提交号>
.
如果你想将一些提交复制到当前所在的位置(HEAD
)下面的话,Cherry-pick是最直接的方式了,它特别简单。
这里有一个仓库,我们想将side分支上的工作复制到 main
分支,你立刻想到了之前学过的rebase
了吧?但是咱们还是看看cherry-pick有什么本领吧。
git cherry-pick C2 C4
这就是了!我们只需要提交记录c2
和c4
,所以Git就将被它们抓过来放到当前分支下了。就是这么简单!
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时,用cherry-pick再好不过了―没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢?幸好Git帮你想到了这一点,我们可以利用交互式的 rebase —如果你想从一系列的提交记录中找到想要的记录,这就是最好的方法了
交互式rebase指的是使用带参数--interactive
的 rebase命令,简写为-i
。
如果你在命令后增加了这个选项,Git 会打开一个UI界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
在实际使用时,所谓的UI窗口一般会在文本编辑器―如 Vim―中打开一个文件。
当rebase UI界面打开时,你能做3件事:
pick
的状态来完成,关闭就意味着你不想要这个提交记录)执行上面命令后,会出现一个交互对话框。对提交记录做个排序(当然你也可以删除某些提交),点击确定看结果。
一个在开发中经常会遇到的情况:我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。
这些调试和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因,解决掉以后觉得沾沾自喜!
最后就差把bugFix分支里的工作合并回main分支了。你可以选择通过 fast-forward 快速合并到
main分支上,但这样的话main分支就会包含我这些调试语句了。你肯定不想这样,应该还有更好的方式…
实际我们只要让 Git复制解决问题的那一个提交记录就可以了。跟之前我们在*"整理提交记录”中学到的一样,我们可以使用
git rebase -i
git cherry-pick
git checkout main
git cherry-pick c4
接下来这种情况也是很常见的:你之前newImage
分支上进行了一次提交,然后又基于它创建了caption
分支,然后又提交了一次。
此时你想对某个以前的提交记录进行一些小小的调整。比如设计师想修改一下newlmage
中图片的分辨率,尽管那个提交记录并不是最新的了。
我们可以通过下面的方法来克服困难:
git rebase -i
将提交重新排序,然后把我们想要修改的提交记录挪到最前git commit —-amend
来进行一些小修改git rebase -i
来将他们调回原来的顺序当然完成这个任务的方法不止上面提到的一种(我知道你在看cherry-pick啦),之后我们会多点关注这些技巧啦,但现在暂时只专注上面这种方法。最后有必要说明一下目标状态中的那几个'
一我们把这个提交移动了两次,每移动一次会产生一个'
;而c2上多出来的那个是我们在使用了amend 参数提交时产生的,所以最终结果就是这样了。
也就是说,我在对比结果的时候只会对比提交树的结构,对于'
的数量上的不同,并不纳入对比范围内。只要你的main
分支结构与目标结构相同,我就算你通过。
git rebase -i HEAD~2
交换c2 c3
git commit --amend
git rebase -i HEAD~2
交换c2 c3
git checkout main
git rebase caption