Git Branch
Git 可视化操作
可视化教程地址:https://learngitbranching.js.org/?locale=zh_CN
基础
git commit
git commit 可以在当前分支上创建一个提交记录。
提交之前,在 c2
节点上:
执行两次 git commit
之后:
我们的节点向前走了两步。现在在 c4
节点上。
这里的 C
代表了节点的哈希值。main
是分支的名称,*
代表了我们当前处于这个分支上。
git branch
git branch <name>
可以创建一个分支。创建的分支会包含原分支的所有节点(相当于复制一份)。
git branch -b <name>
可以创建一个分支,并且切换到那个分支上去。
git checkout <name>
可以手动切换到某个分支上去。(未来的版本中,这个命令会被废弃,取而代之的是:git switch
命令)
想要从图1变成图2,需要这样做:
git branch bugFix
git checkout bugFix
也可以写成一句话:
git checkout -b bugFix
git merge
git merge 可以将两个分支合并在一起
从图1到图2:
$ git checkout -b bugFix # 创建并切换分支 bugFix,此时位于 c1
$ git commit # bugFix 上提交,位于 C2
$ git checkout main # main 分支,依然位于c1
$ git commit # main 分支上创建提交,位于 c3
$ git merge bugFix # 将 bugFix 合并到当前的 main 上,位于 C4
git rebase
git merge 后的分支,会显示分叉的节点,分支结构显得混乱。git rebase 也是用来合并分支的,但是它合并后的结果,并不会显示分叉的路径,而是呈现一个线性的分支。
$ git checkout -b bugFix # 创建切换分支, 新分支 bugFix 也位于 C1 节点上
$ git commit # bugFix 上提交,位于 c2
$ git checkout main # 回到 main 分支:C1
$ git commit # main 创建 C3 节点
$ git checkout bugFix # 切回 bugFix 分支
$ git rebase main # 切记:将当前的 bugFix 分支 rebase 到 main 上。
上图2中
C2'
只是 bugFix 分支上C2
节点的副本而已。如果想要 main 分支也推进到
C2'
节点上,需要先切换到 main 分支,然后使用命令:git rebase bugFix
高级
分离 HEAD
HEAD
是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。HEAD
通常情况下是指向分支名的(如 bugFix)。 HEAD
总是指向当前分支上最近一次提交记录,并随着每次提交而向前移动
从图1 -> 图2:
git checkout c4
通过这种方式,我们将 HEAD
和当前分支分离开了。想要切回去,执行 git checkout bugFix
就回到分支上了。
分离头部的作用:当分离 HEAD 时,你此时不处于任何分支上面,即游离状态。官方的说法是这样的:
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
你现在处于
分离HEAD
状态。你可以四处看看,做一些实验性的改变并且提交它们。你可以通过切换到一个分支来扔掉这些commit,这不会对其他分支产生任何影响。如果你想创建一个分支来保存你创建的 commit,你可以使用
git switch -c <new-branch-name>
的命令。或者你也可以使用
git switch -
来直接切换到上次的分支上去。
相对引用
上面使用哈希值来移动 HEAD
,但是哈希值很难记,因此我们可以使用 相对引用
的方式,来操作它。
^
代表了移动几个节点,譬如:main^
代表移动到 main
分支的父节点(上一个节点),main^^
代表移动上两个节点...
~3
这种方式也可以快速移动到上几个节点,譬如:HEAD~3
表示移动到上三个父节点。
git checkout bugFix
git checkout HEAD^
相对引用 2
git branch -f <分支名> 节点
可以直接将某个分支指向某个节点
上面使用了 ^
来移动HEAD,但是如果需要移动很多次,则需要输入多个 ^
,也挺烦人,于是可以使用 ~3
这种方式,来移动 3
次
$ git branch -f main c6 # 将 main 分支指向 C6
$ git branch -f bugFix c0 # bugFix 指向 C0 节点
$ git checkout HEAD~1 # 将 HEAD 指向上一个节点,即 C1
撤销更改
git reset
可以撤销更改,相当于回退到某个节点,中间回退的这些节点,相当于删除消失了。
git revert
也可以撤销更改,但它会新产生一个节点,譬如你 revert 当前节点。那么 revert 命令会产生一个新节点,这个节点正好是当前节点对上个节点的相反操作。譬如当前节点相对于上个节点添加了个文件,则 revert 后新产生的节点会删除这个文件,总之就是它的逆向操作。
$ git reset HEAD^ # 撤销当前 local 分支的记录到上个节点(此时 C3 是半透明不可见的状态)
$ git checkout pushed # 切换分支
$ git revert C2 # revert 到某个节点上,现在新产生的 C2' 其实和 C1 是一样的。
杂项
cherry-pick
如果你想将一些提交复制到当前所在的位置(HEAD
)后面的话, Cherry-pick 是最直接的方式了。它的作用很简单,将某些提交记录从一个分支中摘出来,放到当前分支上。
使用场景:譬如你在 bug 分支上修复了某个bug,你可以直接在主分支上把修复了 bug 的那个 commit 摘过来,就行了。
git cherry-pick c3 c4 c7 # 将 c3, c4, c7 摘出来,放到当前 main 分支上
交互式 rebase
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 —— 没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase
交互式 rebase 指的是使用带参数 --interactive
的 rebase 命令, 简写为 -i
, 它会打开一个编辑框(vi 的编辑界面),你可以 pick
自己想要的记录,同时还能调整 pick 节点的顺序。
git rebase -i HEAD~4 # 回到 C1 节点,开始选择 C1 之后的节点
# 在弹出的vi编辑框内,你可以调整 pick 节点的顺序,也可以 drop 某个节点,调整完以后保存就行了
# 这里我们 pick c3, pick c5, pick c4
技巧1 调整节点顺序
现有如下场景:caption 分支是基于 newImage 分支创建的,现在 caption 位于 C3 节点,但是我们想要修改 C2 节点的内容,怎么做?
提示:git commit --amend
可以修改节点,而不产生新的节点。
$ git rebase -i HEAD^^ # 在vi编辑界面,重新规划节点的顺序,将 C2 调整成最新的节点(C2,C3 顺序换一下)
$ git commit --amend # 在 C2 上修改提交
$ git rebase -i HEAD^^ # 在vi编辑界面,重新规划节点,将 C3 恢复成最新的节点
$ git branch -f main c3'' # 将 main 分支挪到最新的节点上
技巧2
针对 技巧1 章节的问题,使用 cherry-pick 命令
git checkout main
git cherry-pick c2 c3
用这个就很简单了,直接用就行
git tag
相信通过前面课程的学习你已经发现了:分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。
你可能会问了:有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
Git 的 tag 就是干这个用的,它们可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。
更难得的是,它们并不会随着新的提交而移动。你也不能切换到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
语法:git tag <label> 节点哈希
,如果不指定哈希,则默认会在当前 HEAD 处创建标签
$ git tag v0 c1
$ git tag v1 c2
$ git checkout c2 # 将 HEAD 移动到 C2
多分支 rebase
$ git rebase main bugFix # 将 bugFix 分支合并到 main 分支上。
$ git rebase bugFix side
$ git rebase side another
$ git rebase another main
git rebase 有两种写法:
git rebase <分支>
会将当前分支合并到指定的分支上;git rebase <分支1> <分支2>
会将分支2合并到分支1上
两个父节点
如果一个节点只有一个父节点,我们可以很轻松的移动到那个节点:git checkout HEAD^
,但是如果它有两个父节点呢?
^
后面还可以跟随一个数字,它代表了我们选择的第几个父节点(不加数字时默认选择当前节点正上方的父节点,这是默认的第一个节点)
git checkout HEAD~^2~
这里,~, ^
可以混用。上面的代码中,HEAD~^2~
可以拆分成:HEAD~, HEAD^2, HEAD~
,即先走到 C6
,然后选择 ^2
代表了第二个分支即 C5
节点(第一个分支为正上方一条直线的分支),然后又走了1个节点到 C2
纠缠的分支
$ git branch -f three c2
$ git checkout one
$ git cherry-pick c4 c3 c2
$ git checkout two
$ git cherry-pick c5 c4 c3 c2
远程仓库
git clone
git clone 可以让你在你的本地环境复制一份远程仓库。
上图中,虚线的仓库代表了远程的仓库。左侧实心仓库代表了本地的仓库副本
git clone
远程分支
你可能注意到的第一个事就是在我们的本地仓库多了一个名为 o/main
的分支, 这种类型的分支就叫远程分支,它反映了远程分支的状态。由于远程分支的特性导致其拥有一些特殊属性。
你可能想问这些远程分支的前面的 o/
是什么意思呢?好吧, 远程分支有一个命名规范 —— 它们的格式是:
<remote name>/<branch name>
因此,如果你看到一个名为 o/main
的分支,那么这个分支就叫 main
,远程仓库的名称就是 o
。
大多数的开发人员会将它们主要的远程仓库命名为 origin
,并不是 o
。这是因为当你用 git clone
某个仓库时,Git 已经帮你把远程仓库的名称设置为 origin
了
$ git commit # main 分支上进行提交
$ git checkout o/main # 切换到 远程分支
$ git commit # 提交以后创建了 C4 节点,注意:远程分支上创建节点,会自动进入分离 HEAD 状态,即分支不会随着 HEAD 一起向前移动。这是因为你做的更改都是本地仓库上的更改,远程分支的状态还停留在原地。
git fetch
git fetch 做了些什么
git fetch
完成了仅有的但是很重要的两步:
- 从远程仓库下载本地仓库中缺失的提交记录
- 更新远程分支指针(如
o/main
)
git fetch
实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。
git fetch
通常通过互联网(使用 http://
或 git://
协议) 与远程仓库通信。
git fetch 不会做的事
git fetch
并不会改变你本地仓库的状态。它不会更新你的 main
分支,也不会修改你磁盘上的文件。
理解这一点很重要,因为许多开发人员误以为执行了 git fetch
以后,他们本地仓库就与远程仓库同步了。它可能已经将进行这一操作所需的所有数据都下载了下来,但是并没有修改你本地的文件。我们在后面的课程中将会讲解能完成该操作的命令.所以, 你可以将 git fetch
的理解为单纯的下载操作。
git fetch
git pull
git pull 其实就是 git fetch
和 git merge
的组合。它的作用就是先下载远程分支,然后将其合并到本地的主分支上。因此 git pull
才会真的修改你本地仓库的内容。
$ git fetch
$ git merge o/main
上面的命令可以合起来写成:
$ git pull
团队协作
$ git clone
$ git fakeTeamwork 2 # 模拟远程仓库上创建的2个节点(现实情况没有这个命令),即别人推送到远程上两个记录
$ git commit # 本地也提交了一个 C4
$ git pull # pull 会更新远程分支,并和当前分支合并
git push
git push 可以将本地的分支,更新推送到远程分支上。
$ git commit # main 分支提交一个记录 C2
$ git commit # main 分支提交一个记录 C3
$ git push # 把这些新的节点推送到远程仓库
pull / push 失败
git pull、git push 都会尝试合并分支。譬如你周一下载了远程仓库,在本地进行了修改,等到周五时,尝试 push 到远程仓库,但是远程仓库已经被人更新了,并且新的内容和你要提交的内容有冲突,因此你此时是无法 Push 成功的。
这时你需要先 git pull
来拉取代码,解决掉冲突的部分,然后再尝试 push。
git pull
还有一个参数,git pull --rebase
可以选择 pull 的时候不使用默认的 merge
,而是 rebase
。
跟踪分支
在前几节课程中有件事儿挺神奇的,Git 好像知道 main
与 o/main
是相关的。当然这些分支的名字是相似的,可能会让你觉得是依此将远程分支 main 和本地的 main 分支进行了关联。这种关联在以下两种情况下可以清楚地得到展示:
- pull 操作时, 提交记录会被先下载到 o/main 上,之后再合并到本地的 main 分支。隐含的合并目标由这个关联确定的。
- push 操作时, 我们把工作从
main
推到远程仓库中的main
分支(同时会更新远程分支o/main
) 。这个推送的目的地也是由这种关联确定的!
直接了当地讲,main
和 o/main
的关联关系就是由分支的“remote tracking”属性决定的。main
被设定为跟踪 o/main
—— 这意味着为 main
分支指定了推送的目的地以及拉取后合并的目标。
你可能想知道 main
分支上这个属性是怎么被设定的,你并没有用任何命令指定过这个属性呀!好吧, 当你克隆仓库的时候, Git 就自动帮你把这个属性设置好了。
当你克隆时, Git 会为远程仓库中的每个分支在本地仓库中创建一个远程分支(比如 o/main
)。然后再创建一个跟踪远程仓库中活动分支的本地分支,默认情况下这个本地分支会被命名为 main
。
克隆完成后,你会得到一个本地分支(如果没有这个本地分支的话,你的目录就是“空白”的),但是可以查看远程仓库中所有的分支(如果你好奇心很强的话)。这样做对于本地仓库和远程仓库来说,都是最佳选择。
这也解释了为什么会在克隆的时候会看到下面的输出:
local branch "main" set to track remote branch "o/main"
我能自己指定这个属性吗?
当然可以啦!你可以让任意分支跟踪 o/main
, 然后该分支会像 main
分支一样得到隐含的 push 目的地以及 merge 的目标。 这意味着你可以在分支 totallyNotMain
上执行 git push
,将工作推送到远程仓库的 main
分支上。
有两种方法设置这个属性,第一种就是通过远程分支检出一个新的分支,执行:
git checkout -b totallyNotMain o/main
就可以创建一个名为 totallyNotMain
的分支,它跟踪远程分支 o/main
。
另一种设置远程追踪分支的方法就是使用:git branch -u
命令,执行:
git branch -u o/main foo
这样 foo
就会跟踪 o/main
了。如果当前就在 foo 分支上, 还可以省略 foo:
git branch -u o/main
Git Push 的参数
很好! 既然你知道了远程跟踪分支,我们可以开始揭开 git push、fetch 和 pull 的神秘面纱了。我们会逐个介绍这几个命令,它们在理念上是非常相似的。
首先来看 git push
。在远程跟踪课程中,你已经学到了 Git 是通过当前检出分支的属性来确定远程仓库以及要 push 的目的地的。这是未指定参数时的行为,我们可以为 push 指定参数,语法是:
git push <remote> <place>
先看看例子, 这个命令是:
git push origin main
把这个命令翻译过来就是:
切到本地仓库中的“main”分支,获取所有的提交,再到远程仓库“origin”中找到“main”分支,将远程仓库中没有的提交记录都添加上去。
意思很简单,我们告诉 push 命令:仓库地址,本地分支。它就会将本地的这个分支推送到远程仓库中相应的分支上去。
另一个参数:
git push origin <source>:<destination>
意思是:把本地的 source 推送到远程的 destination 上。如果 destination 不存在,则会在远程仓库中创建这个分支
譬如:
git push origin main:newBranch
会把本地的 main 分支推送到远程的 newBranch 分支上,如果远程仓库没有这个分支,则会主动创建它(当然,本地也会多出一个 origin/newBranch 远程分支)
git fetch 参数
git fetch
的参数和 git push
极其相似。他们的概念是相同的,只是方向相反罢了(因为现在你是下载,而非上传)
如果你像如下命令这样为 git fetch 设置 的话:
git fetch origin foo
Git 会到远程仓库的 foo
分支上,然后获取所有本地不存在的提交,放到本地的 o/foo
上而不是放到本地的 foo 分支,它不会更新你的本地的非远程分支, 只是下载提交记录。
指定 <source>:<destination>
这里有一点是需要注意的 —— source
现在指的是远程仓库中的位置,而 <destination>
才是要放置提交的本地仓库的位置。它与 git push 刚好相反,这是可以讲的通的,因为我们在往相反的方向传送数据。
git fetch origin foo:bar
上面的例子中,我们制定了目的地:bar,它会将远程的 foo 分支下载到了本地的
bar
分支(一个本地分支)上。注意由于我们指定了目标分支,foo
和o/foo
都没有被更新。
跟 git push 一样,Git 会在 fetch 前自己创建立本地分支, 就像是 Git 在 push 时,如果远程仓库中不存在目标分支,会自己在建立一样。
如果 fetch
不带参数,它会下载所有远程仓库的分支的提交记录到本地的各个远程分支……
古怪的 <source>
Git 有两种关于 <source>
的用法是比较诡异的,即你可以在 git push 或 git fetch 时不指定任何 source
,方法就是仅保留冒号和 destination 部分,source 部分留空。
git push origin :side
git fetch origin :bugFix
我们分别来看一下这两条命令的作用……
如果 push 空 到远程仓库会如何呢?它会删除远程仓库中的分支!
git push origin :foo # 删除远程的 foo 分支
就是这样子, 我们通过给 push 传空值 source,成功删除了远程仓库中的 foo
分支, 这真有意思...
如果 fetch 空 到本地,会在本地创建一个新分支。
git fetch origin :bar # 在本地创建一个 bar 分支
Git pull 参数
既然你已经掌握关于 git fetch
和 git push
参数的方方面面了,关于 git pull 几乎没有什么可以讲的了 😃
因为 git pull 到头来就是 fetch 后跟 merge 的缩写。你可以理解为用同样的参数执行 git fetch,然后再 merge 你所抓取到的提交记录。
还可以和其它更复杂的参数一起使用, 来看一些例子:
以下命令在 Git 中是等效的:
git pull origin foo
相当于:
git fetch origin foo; git merge o/foo
还有...
git pull origin bar~1:bugFix
相当于:
git fetch origin bar~1:bugFix; git merge bugFix
看到了? git pull 实际上就是 fetch + merge 的缩写, git pull 唯一关注的是提交最终合并到哪里(也就是为 git fetch 所提供的 destination 参数)
一起来看个例子吧:
如果我们指定要抓取的 place,所有的事情都会跟之前一样发生,只是增加了 merge 操作
git pull origin main
通过指定
main
我们更新了o/main
。然后将o/main
merge 到我们的检出位置,无论我们当前检出的位置是哪(即无论当前分支是什么,都会merge到当前分支)
pull 也可以用 source:destination 吗? 当然喽, 看看吧:
git pull origin main:foo
哇, 这个命令做的事情真多。它先在本地创建了一个叫 foo
的分支,从远程仓库中的 main 分支中下载提交记录,并合并到 foo
,然后再 merge 到我们的当前检出的分支上。操作够多的吧!