LOADING

加载过慢请开启缓存 浏览器默认开启

Learning Git Branching 通关

2024/1/19 教程 git

好耶、边玩边学!!!

Learning Git Branching游玩地址:https://learngitbranching.js.org/?locale=zh_CN

我在思考需不需要将知识点总结写出来,但是我觉得这个网页的闯关式教学就已经挺好的。于是,我决定只保留每一关的问题与我的答案。Q表示问题,A表示答案。就当是一个小项目的分享。

强烈建议直接进入网站游玩闯关。

Git commit

Q

进行两次提交

A

git commit
git commit

Git branch

Q

创建一个名为 bugFix 的新分支,然后切换过去。

A

git branch bugFix
git checkout bugFix

or

git checkout -b bugFix

Git merge

Q

创建新分支 bugFix,切换到该分支,提交一次

切换回 main,再提交一次

bugFix 合并到 main

A

git checkout -b bugFix
git commit
git checkout main
git commit
git merge bugFix

Git rebase

Q

新建并切换到 bugFix 分支,提交一次

切换回 main 分支再提交一次

再次切换到 bugFix 分支,rebase 到 main

A

git checkout -b bugFix
git commit
git checkout main
git commit
git checkout bugFix
git rebase main

分离HEAD

Q

bugFix 分支中分离出 HEAD 并让其指向一个提交记录。

通过哈希值指定提交记录。每个提交记录的哈希值显示在代表提交记录的圆圈中。

A

git checkout c4

相对引用(^)

Q

切换到 bugFix 的 parent 节点。这会进入分离 HEAD 状态。

A

git checkout bugFix^

相对引用2(~)

Q

移动 HEADmainbugFix 到目标所示的位置。

A

git checkout HEAD^
git branch -f bugFix HEAD^
git branch -f main c6

or

git branch -f bugFix HEAD~2
git checkout HEAD^
git branch -f main c6

撤销变更

Q

分别撤销 local 分支和 pushed 分支上的最近一次提交。共需要撤销两个提交(每个分支一个)。

记住 pushed 是远程分支,local 是本地分支

A

git reset local^
git checkout pushed
git revert pushed

Git cherry-pick

Q

选取其他分支的指定提交记录,合并到main分支

A

git cherry-pick c3 c4 c7

交互式 rebase

Q

当 rebase UI界面打开时, 你能做3件事:

  • 调整提交记录的顺序(通过鼠标拖放来完成)
  • 删除你不想要的提交(通过切换 pick 的状态来完成,关闭就意味着你不想要这个提交记录)
  • 合并提交。

做一次交互式的 rebase,整理成目标窗口中的提交顺序。

A

git rebase -i overHere
调整按题目顺序即可

只取一个提交记录

Q

确保 main 分支能得到 bugFix 分支上的相关提交。

A

git rebase -i main
git checkout main
git cherry-pick bugFix

or

git rebase -i main
git rebase bugFix main

提交的技巧1

Q

你之前在 newImage 分支上进行了一次提交,然后又基于它创建了 caption 分支,然后又提交了一次。

此时你想对某个以前的提交记录进行一些小小的调整。比如设计师想修改一下 newImage 中图片的分辨率,尽管那个提交记录并不是最新的了。

通过下面的方法来克服困难:

  • 先用 git rebase -i 将提交重新排序,然后把我们想要修改的提交记录挪到最前
  • 然后用 git commit --amend 来进行一些小修改
  • 接着再用 git rebase -i 来将他们调回原来的顺序
  • 最后我们把 main 移到修改的最前端(用你自己喜欢的方法),就大功告成啦

A

git rebase -i HEAD~2
git commit --amend
git rebase -i main
git rebase caption main

提交的技巧 2

Q

可以使用 rebase -i 对提交记录进行重新排序。只要把我们想要的提交记录挪到最前端,我们就可以很轻松的用 --amend 修改它,然后把它们重新排成我们想要的顺序。

但这样做就唯一的问题就是要进行两次排序,而这有可能造成由 rebase 而导致的冲突。下面还是看看 git cherry-pick 是怎么做的吧。

目标和上一关一样,通过 --amend 改变提交记录 C2,但你不能用 rebase -i

A

git checkout main
git cherry-pick c2
git commit --amend
git cherry-pick c3

Git Tag

Q

按照目标建立两个标签,然后切换到 v1 上面,要注意你会进到分离 HEAD 的状态 —— 这是因为不能直接在v1 上面做 commit。

A

git checkout c2
git tag v0 HEAD^
git tag v1

Git Describe

Q

git describe 的语法是:

git describe <ref>

<ref> 可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会使用你目前所在的位置(HEAD)。

它输出的结果是这样的:

<tag>_<numCommits>_g<hash>

tag 表示的是离 ref 最近的标签, numCommits 是表示这个 reftag 相差有多少个提交记录, hash 表示的是你所给定的 ref 所表示的提交记录哈希值的前几位。

ref 提交记录上有某个标签时,则只输出标签名称

试着在这个关卡指定几个位置来感受一下这个命令

A

git describe main
git describe side
git describe bugFix
git commit

多次 Rebase

Q

把所有这些分支上所做的工作都通过 rebase 合并到 main 分支上吧。

但是你的领导给你提了点要求 —— 他们希望得到有序的提交历史,也就是我们最终的结果应该是 C6'C7' 上面, C5'C6' 上面,依此类推。

A

git rebase main bugFix
git rebase bugFix side
git rebase side another
git rebase another main

两个 parent 节点

Q

操作符 ^~ 符一样,后面也可以跟一个数字。

但是该操作符后面的数字与 ~ 后面的不同,并不是用来指定向上返回几代,而是指定合并提交记录的某个 parent 提交。还记得前面提到过的一个合并提交有两个 parent 提交吧,所以遇到这样的节点时该选择哪条路径就不是很清晰了。

Git 默认选择合并提交的“第一个” parent 提交,在操作符 ^ 后跟一个数字可以改变这一默认行为。这些操作符还支持链式操作!

在指定的目标位置创建一个新的分支。

很明显可以简单地直接使用提交记录的哈希值(比如 C6),但我要求你使用刚刚讲到的相对引用修饰符

A

git branch -f bugWork c2

or

git branch bugWork main^^2^

纠缠不清的分支

Q

现在我们的 main 分支是比 onetwothree 要多几个提交。出于某种原因,我们需要把 main 分支上最近的几次提交做不同的调整后,分别添加到各个的分支上。

one 需要重新排序并删除 C5two 仅需要重排排序,而 three 只需要提交一次。

A

git checkout one
git cherry-pick c4 c3 c2
git checkout two
git cherry-pick c5 c4 c3 c2
git checkout three
git rebase c2 three

or

git checkout one
git cherry-pick c4 c3 c2
git checkout two
git cherry-pick c5 c4 c3 c2
git branch -f three c2

Git clone

从技术上来讲,git clone 命令在真实的环境下的作用是在本地创建一个远程仓库的拷贝(比如从 github.com)。 但在该教程中使用这个命令会有一些不同 —— 它会在远程创建一个你本地仓库的副本。显然这和真实命令的意思刚好相反,但是它帮咱们把本地仓库和远程仓库关联到了一起,在教程中就凑合着用吧。

Q

克隆仓库

A

git clone

远程分支

Q

main 分支上做一次提交;然后切换到 o/main,再做一提交。这有助于你理解远程分支的不同,他们的更新只是反映了远程的状态。

A

git commit
git checkout o/main
git commit

Git Fetch

Q

git fetch 完成了仅有的但是很重要的两步:

  • 从远程仓库下载本地仓库中缺失的提交记录
  • 更新远程分支指针(如 o/main)

git fetch 实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。

git fetch 并不会改变你本地仓库的状态。它不会更新你的 main 分支,也不会修改你磁盘上的文件。

本关只需用 git fetch 下载所有的提交!

A

git fetch

Git Pull

Q

git pull 就是 git fetch 和 git merge 的缩写!接下来会探索一下 git pull 的细节(包括选项和参数),现在咱们先解决这个关卡。

实际上你完全可以用 fetchmerge 通过本关,但是这会增加你的命令数。

A

git pull

模拟团队合作

Q

为了接下来的课程, 我们需要先教你如何制造远程仓库的变更。

这意味着,我们需要“假装”你的同事、朋友、合作伙伴更新了远程仓库,有可能是某个特定的分支,或是几个提交记录。

注意:该网页为了做到这点,引入一个自造命令 git fakeTeamwork

fakeTeamwork 默认操作就是在远程仓库的 main 分支上做一次提交。可以指定提交的分支或是数量,只需要在命令后加上它们就可以了。

克隆一个远程仓库(用 git clone),再在刚创建的远程仓库中模拟一些修改,然后在你自己的本地分支上做一些提交,再拉取远程仓库的变更。

A

git clone
git fakeTeamwork 2
git commit
git pull

Git Push

Q

git pull 相反的命令是什么呢?git push

git push 负责将你的变更上传到指定的远程仓库,并在远程仓库上合并你的新提交记录。一旦 git push 完成, 你的朋友们就可以从这个远程仓库下载。

要完成本关,需要向远程仓库分享两个提交记录。

A

git commit
git commit
git push

偏离的提交历史

Q

我们用 git fetch 更新了本地仓库中的远程分支,然后用 rebase 将我们的工作移动到最新的提交记录下,最后再用 git push 推送到远程仓库。还可以使用 merge,尽管 git merge 不会移动你的工作(它会创建新的合并提交),但是它会告诉 Git 你已经合并了远程仓库的所有变更。这是因为远程分支现在是你本地分支的祖先,也就是说你的提交已经包含了远程分支的所有变化。前面已经介绍过 git pull 就是 fetch 和 merge 的简写,类似的 git pull --rebase 就是 fetch 和 rebase 的简写!

要完成本关,你需要完成以下几步:

  • 克隆你的仓库
  • 模拟一次远程提交(fakeTeamwork)
  • 完成一次本地提交
  • rebase 发布你的工作

A

git clone
git fakeTeamwork
git commit
git pull --rebase
git push

锁定的Main(Locked Main)

Q

如果你是在一个大的合作团队中工作, 很可能是main被锁定了, 需要一些Pull Request流程来合并修改。如果你直接提交(commit)到本地main, 然后试图推送(push)修改, 你将会收到这样类似的信息:

! [远程服务器拒绝] main -> main (TF402455: 不允许推送(push)这个分支; 你必须使用pull request来更新这个分支.)

远程服务器拒绝直接推送(push)提交到main, 因为策略配置要求 pull requests 来提交更新.

你应该按照流程,新建一个分支, 推送(push)这个分支并申请pull request,但是你忘记并直接提交给了main.现在你卡住并且无法推送你的更新.

解决办法:新建一个分支feature, 推送到远程服务器. 然后reset你的main分支和远程服务器保持一致, 否则下次你pull并且他人的提交和你冲突的时候就会有问题.

A

注意:该网页中默认的行为是--hard硬重置,但是要记录Git中默认的是--mixed

git reset --hard o/main
git checkout -b feature c2
git push

推送主分支

Q

在大型项目中开发人员通常会在(从 main 上分出来的)特性分支上工作,工作完成后只做一次集成。这跟前面课程的描述很相像(把 side 分支推送到远程仓库),不过本节我们会深入一些.

但是有些开发人员只在 main 上做 push、pull —— 这样的话 main 总是最新的,始终与远程分支 (o/main) 保持一致。

对于接下来这个工作流,我们集成了两个步骤:

  • 将特性分支集成到 main
  • 推送并更新远程分支

快速的更新 main 分支并推送到远程:

  • 将我们的工作 rebase 到远程分支的最新提交记录
  • 向远程仓库推送我们的工作

这个关卡的 Boss 很厉害 —— 以下是通关提示:

  • 这里共有三个特性分支 —— side1 side2side3
  • 我需要将这三分支按顺序推送到远程仓库
  • 因为远程仓库已经被更新过了,所以我们还要把那些工作合并过来

A

git checkout main
git pull --rebase
git rebase main side1
git rebase side1 side2
git rebase side2 side3
git rebase side3 main
git push

or

git fetch
git rebase o/main side1
git rebase side1 side2
git rebase side2 side3
git rebase side3 main
git push

合并远程仓库

Q

在开发社区里,有许多关于 merge 与 rebase 的讨论。以下是关于 rebase 的优缺点:

优点:

  • Rebase 使你的提交树变得很干净, 所有的提交都在一条线上

缺点:

  • Rebase 修改了提交树的历史

比如, 提交 C1 可以被 rebase 到 C3 之后。这看起来 C1 中的工作是在 C3 之后进行的,但实际上是在 C3 之前。

一些开发人员喜欢保留提交历史,因此更偏爱 merge。而其他人可能更喜欢干净的提交树,于是偏爱 rebase。

本关,我们还是解决上一关卡中的问题,但是要用 merge 替换 rebase。这显然有点画蛇添足,但这只是为了更好的说明上面的观点。

A

git checkout main
git pull
git merge side1
git merge side2
git merge side3
git push

远程跟踪分支

Q

Git 好像知道 maino/main 是相关的。当然这些分支的名字是相似的,可能会让你觉得是依此将远程分支 main 和本地的 main 分支进行了关联。这种关联在以下两种情况下可以清楚地得到展示:

  • pull 操作时, 提交记录会被先下载到 o/main 上,之后再合并到本地的 main 分支。隐含的合并目标由这个关联确定的。
  • push 操作时, 我们把工作从 main 推到远程仓库中的 main 分支(同时会更新远程分支 o/main) 。这个推送的目的地也是由这种关联确定的!

直接了当地讲,maino/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

本节我们在切换到 main 分支的情况下将工作推送到的远程仓库中的 main 分支上。

A

git checkout -b side o/main
git commit
git pull --rebase
git push

Git push 的参数1

Q

既然你知道了远程跟踪分支,我们可以开始揭开 git push、fetch 和 pull 的神秘面纱了。我们会逐个介绍这几个命令,它们在理念上是非常相似的。

首先来看 git push。在远程跟踪课程中,你已经学到了 Git 是通过当前所在分支的属性来确定远程仓库以及要 push 的目的地的。这是未指定参数时的行为,我们可以为 push 指定参数,语法是:

git push <remote> <place>

<place> 参数是什么意思呢?我们稍后会深入其中的细节, 先看看例子, 这个命令是:

git push origin main

把这个命令翻译过来就是:

切到本地仓库中的“main”分支,获取所有的提交,再到远程仓库“origin”中找到“main”分支,将远程仓库中没有的提交记录都添加上去,搞定之后告诉我。

我们通过“place”参数来告诉 Git 提交记录来自于 main, 要推送到远程仓库中的 main。它实际就是要同步的两个仓库的位置。

需要注意的是,因为我们通过指定参数告诉了 Git 所有它需要的信息, 所以它就忽略了我们所切换分支的属性!

本关我们要更新远程仓库中的 foomain, 但是 git checkout 被禁用了!

注意:远程分支使用 o/ 开头是因为 origin/ 对于 UI 来说太长了。不用太在意这个,直接用 origin 作为远程仓库的名称就可以了。

A

git push origin main
git push origin foo

Git push 参数 2

Q

当为 git push 指定 place 参数为 main 时,我们同时指定了提交记录的来源和去向。

你可能想问 —— 如果来源和去向分支的名称不同呢?比如你想把本地的 foo 分支推送到远程仓库中的 bar 分支。

哎,很遗憾 Git 做不到…… 开个玩笑,别当真!当然是可以的啦 :) Git 拥有超强的灵活性(有点过于灵活了)

要同时为源和目的地指定 <place> 的话,只需要用冒号 : 将二者连起来就可以了:

git push origin <source>:<destination>

这个参数实际的值是个 refspec,“refspec” 是一个自造的词,意思是 Git 能识别的位置(比如分支 foo 或者 HEAD~1

一旦你指定了独立的来源和目的地,就可以组织出言简意赅的远程操作命令.

在这个关卡中,试着完成目标窗口展示的提交树,记住参数格式哟:

<source>:<destination>

A

git push origin foo:main
git push origin main^:foo

Git fetch 的参数

Q

git fetch 的参数和 git push 极其相似。他们的概念是相同的,只是方向相反罢了(因为现在你是下载,而非上传)

如果你像如下命令这样为 git fetch 设置 的话:

git fetch origin foo

Git 会到远程仓库的 foo 分支上,然后获取所有本地不存在的提交,放到本地的 o/foo 上。

为何 Git 会将新提交放到 o/foo 而不是放到我本地的 foo 分支呢?之前不是说这样的 参数就是同时应用于本地和远程的位置吗?

好吧, 本例中 Git 做了一些特殊处理,因为你可能在 foo 分支上的工作还未完成,你也不想弄乱它。还记得在 git fetch 课程里我们讲到的吗 —— 它不会更新你的本地的非远程分支, 只是下载提交记录(这样, 你就可以对远程分支进行检查或者合并了)。

如果你觉得直接更新本地分支很爽,那你就用冒号分隔的 refspec 吧。不过,你不能在当前切换的分支上干这个事,但是其它分支是可以的。

这里有一点是需要注意的 —— source 现在指的是远程仓库中的位置,而 <destination> 才是要放置提交的本地仓库的位置。它与 git push 刚好相反,这是可以讲的通的,因为我们在往相反的方向传送数据。

理论上虽然行的通,但开发人员很少这么做。我在这里介绍它主要是为了从概念上说明 fetchpush 的相似性,只是方向相反罢了。

要完成本关,抓取目标窗口中指定的提交记录,使用这些魔幻的命令吧!

使用 fetch 时, 你必须指定 source 和 destination。 注意一下目标窗口, 因为提交对象的 ID 可能会变哦!

A

git fetch origin foo:main
git fetch origin main^:foo
git checkout foo 
git merge main

没有 source 的 source

Q

Git 有两种关于 <source> 的用法是比较诡异的,即你可以在 git push 或 git fetch 时不指定任何 source,方法就是仅保留冒号和 destination 部分,source 部分留空。

  • git push origin :side
  • git fetch origin :bugFix

我们分别来看一下这两条命令的作用

通过给 push 传空值 source,成功删除了远程仓库中的 foo 分支

fetch 空 到本地,会在本地创建一个新分支。

这个关卡很容易 —— 只要删除一个远程的分支, 再用 git fetch 在本地创建一个新分支就可以了!

A

git push origin :foo
git fetch origin :bar

Git pull 的参数

Q

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 参数)

请按照目标窗口中的状态进行操作。你需要下载一些提交,然后创建一些新分支,再合并这些分支到其它分支, 但这用不了几个命令

A

git fetch origin bar:foo
git fetch origin main:side
git merge foo
git merge side

or

git pull origin bar:foo
git pull origin main:side

Wow!你通关了! (ノ^_^)ノ (ノ^_^)ノ (ノ^_^)ノ

本来以为两个小时左右应该就可以搞定,但没想到不止。不过这么一玩,也知道了Git大致的用法啦!