推荐学习网站:https://learngitbranching.js.org/
🧚♀️ 主要
1. Git Commit
Git 仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!
Git 希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。
Git 还保存了提交的历史记录。这也是为什么大多数提交记录的上面都有父节点的原因 —— 我们会在图示中用箭头来表示这种关系。对于项目组的成员来说,维护提交历史对大家都有好处。
关于提交记录太深入的东西咱们就不再继续探讨了,现在你可以把提交记录看作是项目的快照。提交记录非常轻量,可以快速地在这些提交记录之间切换!
git commit
2. Git Branch
Git 的分支也非常轻量。它们只是简单地指向某个提交纪录 —— 仅此而已。所以许多 Git 爱好者传颂:早建分支!多用分支!
这是因为即使创建再多分的支也不会造成储存或内存上的开销,并且按逻辑分解工作到不同的分支要比维护那些特别臃肿的分支简单多了。
在将分支和提交记录结合起来后,我们会看到两者如何协作。现在只要记住使用分支其实就相当于在说:“我想基于这个提交以及它所有的父提交进行新的工作。”
- 要创建一个到名为 newImage 的分支:
git branch newImage
- 在提交修改之前先切换到新的分支上:
git checkout newImage
如果你想创建一个新的分支同时切换到新创建的分支的话,可以通过
git checkout -b <your-branch-name>
来实现。
3. Git Merge
如何将两个分支合并到一起?就是说我们新建一个分支,在其上开发某个新功能,开发完成后再合并回主线。
先看第一种方法 —— git merge。在 Git 中合并两个分支时会产生一个特殊的提交记录,它有两个父节点。翻译成自然语言相当于:“我要把这两个父节点本身及它们所有的祖先都包含进来。”
- 我们要把 bugFix 合并到 master 里:
git merge bugFix
- 再把 master 分支合并到 bugFix:
git checkout bugFix; git merge master
4. Git Rebase
第二种合并分支的方法是 git rebase。Rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。
Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。
-
我们想要把 bugFix 分支里的工作直接移到 master 分支上:
git rebase master
移动以后会使得两个分支的功能看起来像是按顺序开发,但实际上它们是并行开发的。
-
切换到 master 上。把它 rebase 到 bugFix 分支上:
git rebase bugFix
5. 分离 HEAD
HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。
HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。
HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。
分离的 HEAD
分离的 HEAD 就是让其指向了某个具体的提交记录而不是分支名。在命令执行之前的状态如下所示:
HEAD -> master -> C1
HEAD 指向 master, master 指向 C1
git checkout C1
6. 相对引用(^)
通过指定提交记录哈希值的方式在 Git 中移动不太方便。在实际应用时,并没有像本程序中这么漂亮的可视化提交树供你参考,所以你就不得不用 git log 来查查看提交记录的哈希值。
比较令人欣慰的是,Git 对哈希的处理很智能。你只需要提供能够唯一标识提交记录的前几个字符即可。因此我可以仅输入fed2 而不是上面的一长串字符。
相对引用非常给力,两个简单的用法:
① 使用 ^
向上移动 1 个提交记录
② 使用 ~<num>
向上移动多个提交记录,如 ~3
所以 master^
相当于“master
的父节点”。master^^
是 master
的第二个父节点
- 现在咱们切换到 master 的父节点:
git checkout master^
7. 相对引用(~)
如果你想在提交树中向上移动很多步的话,敲那么多 ^ 貌似也挺烦人的,Git 当然也考虑到了这一点,于是又引入了操作符 ~。
该操作符后面可以跟一个数字(可选,不跟数字时与 ^ 相同,向上移动一次),指定向上移动多少次。
- 用 ~ 一次后退四步:
git checkout HEAD~4
使用相对引用最多的就是移动分支。可以直接使用 -f
选项让分支指向另一个提交。
- 将 master 分支强制指向 HEAD 的第 3 级父提交:
git branch -f master HEAD~3
8. 撤销变更
在 Git 里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。我们这个应用主要关注的是后者。
主要有两种方法用来撤销变更 —— 一是 git reset
,还有就是 git revert
。
git reset
通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset
向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
git reset HEAD~1
Git 把 master 分支移回到 C1;现在我们的本地代码库根本就不知道有 C2 这个提交了。
(注:在reset后, C2 所做的变更还在,但是处于未加入暂存区状态。)
虽然在你的本地分支中使用 git reset 很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦!
- 为了撤销更改并分享给别人,我们需要使用 git revert:
git revert HEAD
9. Git Cherry-pick
git cherry-pick <提交号>...
如果你想将一些提交复制到当前所在的位置(HEAD)下面的话, Cherry-pick 是最直接的方式了。
这里有一个仓库, 我们想将 side 分支上的工作复制到 master 分支,你立刻想到了之前学过的 rebase 了吧?但是咱们还是看看 cherry-pick 有什么本领吧。
- 我们只需要提交记录 C2 和 C4,到当前分支下:
git cherry-pick C2 C4
10. 交互式 rebase
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 —— 没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase —— 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了。
交互式 rebase 指的是使用带参数 --interactive
的 rebase 命令, 简写为 -i
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
在实际使用时,所谓的 UI 窗口一般会在文本编辑器 —— 如 Vim —— 中打开一个文件。
当 rebase UI界面打开时, 你能做 3 件事:
- 调整提交记录的顺序(通过鼠标拖放来完成)
- 删除你不想要的提交(通过切换 pick 的状态来完成,关闭就意味着你不想要这个提交记录)
- 合并提交。它允许你把多个提交记录合并成一个。
git rebase -i HEAD~4
11. 只取一个提交记录
本地栈式提交
🌰:
我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。
这些调试和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因,解决掉以后觉得沾沾自喜!
最后就差把 bugFix 分支里的工作合并回 master 分支了。
你可以选择通过 fast-forward 快速合并到 master 分支上,但这样的话 master 分支就会包含我这些调试语句了。你肯定不想这样,应该还有更好的方式……
实际我们只要让 Git 复制解决问题的那一个提交记录就可以了。可以使用:
git rebase -i
git cherry-pick
12. 提交的技巧(1)
🌰:
你之前在 newImage 分支上进行了一次提交,然后又基于它创建了 caption 分支,然后又提交了一次。
此时你想对的某个以前的提交记录进行一些小小的调整。比如设计师想修改一下 newImage 中图片的分辨率,尽管那个提交记录并不是最新的了。
可以通过下面的方法来克服困难:
- 先用
git rebase -i
将提交重新排序,然后把我们想要修改的提交记录挪到最前; - 然后用
commit --amend
来进行一些小修改; - 接着再用
git rebase -i
来将他们调回原来的顺序; - 最后我们把
master
移到修改的最前端(用你自己喜欢的方法),就大功告成啦!
13. 提交的技巧(2)
我们可以使用 rebase -i
对提交记录进行重新排序。只要把我们想要的提交记录挪到最前端,我们就可以很轻松的用 --amend
修改它,然后把它们重新排成我们想要的顺序。
但这样做就唯一的问题就是要进行两次排序,而这有可能造成由 rebase 而导致的冲突。
cherry-pick
可以将提交树上任何地方的提交记录取过来追加到 HEAD 上(只要不是 HEAD 上游的提交就没问题)。
git cherry-pick C2
14. Git Tag
Git 的 tag 可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。
更难得的是,它们并不会随着新的提交而移动。你也不能检出到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
- 建立一个标签,指向提交记录 C1,表示这是我们 1.0 版本:
git tag v1 C1
如果你不指定提交记录,Git 会用
HEAD
所指向的位置。
15. 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
提交记录上有某个标签时,则只输出标签名称。
![](https://i-blog.csdnimg.cn/blog_migrate/62df348747e11db5c951544eb3aec915.png)
-
git describe master
会输出:v1_2_gC2
-
git describe side
会输出:v2_1_gC4
16. 多次 rebase
这里准备了很多分支,把这些分支 rebase 到 master 上。
但是你的领导给你提了点要求 —— 他们希望得到有序的提交历史,也就是我们最终的结果应该是 C6'
在 C7'
上面, C5'
在 C6'
上面,依此类推。
git rebase master bugFix
git rebase bugFix side
git rebase side another
git rebase another master
17. 两个父节点
选择父提交记录
操作符 ^
与 ~
符一样,后面也可以跟一个数字。
但是该操作符后面的数字与 ~
后面的不同,并不是用来指定向上返回几代,而是指定合并提交记录的某个父提交。还记得前面提到过的一个合并提交有两个父提交吧,所以遇到这样的节点时该选择哪条路径就不是很清晰了。
Git 默认选择合并提交的“第一个”父提交,在操作符 ^
后跟一个数字可以改变这一默认行为。
git checkout master^
git checkout master^2
git checkout HEAD~; git checkout HEAD^2; git chekcout HEAD~2
- 支持链式操作:
git checkout HEAD~^2~2
18. 纠缠不清的分支
现在我们的 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
🧙♂️ 远程
Push & Pull —— Git 远程仓库
1. Git Clone
远程仓库:
远程仓库有一系列强大的特性:
- 首先也是最重要的的点, 远程仓库是一个强大的备份。本地仓库也有恢复文件到指定版本的能力, 但所有的信息都是保存在本地的。有了远程仓库以后,即使丢失了本地所有数据, 你仍可以通过远程仓库拿回你丢失的数据。
- 还有就是, 远程让代码社交化了! 既然你的项目被托管到别的地方了, 你的朋友可以更容易地为你的项目做贡献(或者拉取最新的变更)。
git clone
命令的作用是在本地创建一个远程仓库的拷贝(比如从 github.com)。
2. 远程分支
远程分支有一个命名规范 —— 它们的格式是:<remote name>/<branch name>
因此,如果你看到一个名为 o/master
的分支,那么这个分支就叫 master
,远程仓库的名称就是 o
。
3. Git Fetch
git fetch
—— 从远程仓库获取数据。
远程仓库有两个我们本地仓库中没有的提交。git fetch 之后,C2、C3 被下载到了本地仓库,同时远程分支 o/master 也被更新,反映到了这一变化。
git fetch
完成了仅有的但是很重要的两步:
- 从远程仓库下载本地仓库中缺失的提交记录
- 更新远程分支指针(如
o/master
)
git fetch
实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。
远程分支反映了远程仓库在你最后一次与它通信时的状态,git fetch
就是你与远程仓库通信的方式了!
git fetch
通常通过互联网(使用 http://
或 git://
协议) 与远程仓库通信。
git fetch
并不会改变你本地仓库的状态。它不会更新你的 master
分支,也不会修改你磁盘上的文件。
许多开发人员误以为执行了 git fetch 以后,他们本地仓库就与远程仓库同步了。它可能已经将进行这一操作所需的所有数据都下载了下来,但是并没有修改你本地的文件。所以,你可以将 git fetch
的理解为单纯的下载操作。
4. Git Pull
既然我们已经知道了如何用 git fetch
获取远程的数据, 现在我们学习如何将这些变化更新到我们的工作当中。
其实有很多方法的 —— 当远程分支中有新的提交时,你可以像合并本地分支那样来合并远程分支。也就是说就是你可以执行以下命令:
git cherry-pick o/master
git rebase o/master
git merge o/master
- 等等
实际上,由于先抓取更新再合并到本地分支这个流程很常用,因此 Git 提供了一个专门的命令来完成这两个操作。它就是 git pull
。
git fetch; git merge o/master
==git pull
5. 模拟团队合作
git clone
git fakeTeamwork 2
git commit
git pull
6. Git Push
git push
负责将你的变更上传到指定的远程仓库,并在远程仓库上合并你的新提交记录。
7. 偏离的提交历史
假设你周一克隆了一个仓库,然后开始研发某个新功能。到周五时,你新功能开发测试完毕,可以发布了。但是 —— 天啊!你的同事这周写了一堆代码,还改了许多你的功能中使用的 API,这些变动会导致你新开发的功能变得不可用。但是他们已经将那些提交推送到远程仓库了,因此你的工作就变成了基于项目旧版的代码,与远程仓库最新的代码不匹配了。
这种情况(历史偏离)有许多的不确定性,Git 是不会允许你 push
变更的。实际上它会强制你先合并远程最新的代码,然后才能分享你的工作。
- 先更新本地仓库中的远程分支,然后将我们的工作移动到最新的提交记录下,最后再推送到远程仓库:
git fetch; git rebase o/master; git push
- 或者:使用
merge
尽管git merge
不会移动你的工作(它会创建新的合并提交),但是它会告诉 Git 你已经合并了远程仓库的所有变更。这是因为远程分支现在是你本地分支的祖先,也就是说你的提交已经包含了远程分支的所有变化。- 更新本地仓库中的远程分支,然后合并了新变更到我们的本地分支(为了包含远程仓库的变更),最后把工作推送到远程仓库:
git fetch; git merge o/master; git push
- 更新本地仓库中的远程分支,然后合并了新变更到我们的本地分支(为了包含远程仓库的变更),最后把工作推送到远程仓库:
git pull --rebase
就是fetch
和rebase
的简写!
克隆你的仓库
模拟一次远程提交(fakeTeamwork)
完成一次本地提交
用 rebase 发布你的工作
git clone
git fakeTeamwork
git commit
git pull --rebase
git push
关于 origin 和它的周边 —— Git 远程仓库高级操作
1. 推送主分支
合并特性分支
- 将特性分支集成到
master
上:git pull --rebase
- 推送并更新远程分支:
git push
这里共有三个特性分支 —— side1 side2 和 side3
需要将这三分支按顺序推送到远程仓库
因为远程仓库已经被更新过了,所以我们还要把那些工作合并过来
git fetch
git rebase o/master side1
git rebase side1 side2
git rebase side2 side3
git rebase side3 master
git push
2. 合并远程仓库
关于 rebase 的优缺点:
- 优点:Rebase 使你的提交树变得很干净, 所有的提交都在一条线上
- 缺点:Rebase 修改了提交树的历史(🌰:提交 C1 可以被 rebase 到 C3 之后。这看起来 C1 中的工作是在 C3 之后进行的,但实际上是在 C3 之前。)
一些开发人员喜欢保留提交历史,因此更偏爱 merge
。而其他人可能更喜欢干净的提交树,于是偏爱 rebase
。
利用 merge 完成上面 1 中的例子:
git checkout master
git pull
git merge side1
git merge side2
git merge side3
git push
3. 远程追踪
直接了当地讲,master
和 o/master
的关联关系就是由分支的“remote tracking”属性决定的。master
被设定为跟踪 o/master
—— 这意味着为 master
分支指定了推送的目的地以及拉取后合并的目标。
当你克隆时, Git 会为远程仓库中的每个分支在本地仓库中创建一个远程分支(比如 o/master
)。然后再创建一个跟踪远程仓库中活动分支的本地分支,默认情况下这个本地分支会被命名为 master
。
在克隆的时候会看到下面的输出:local branch "master" set to track remote branch "o/master"
你可以让任意分支跟踪 o/master,然后该分支会像 master 分支一样得到隐含的 push 目的地以及 merge 的目标。 这意味着你可以在分支 totallyNotMaster 上执行 git push,将工作推送到远程仓库的 master 分支上。
有两种方法设置这个属性(设置远程追踪分支的方法):
- 通过远程分支检出一个新的分支。
- 创建一个名为 totallyNotMaster 的分支,它跟踪远程分支 o/master:
git checkout -b totallyNotMaster o/master
- 创建一个名为 totallyNotMaster 的分支,它跟踪远程分支 o/master:
- 使用
git branch -u
命令。- 让 foo 跟踪 o/master :
git branch -u o/master foo
- 如果当前就在 foo 分支上,还可以省略 foo:
git branch -u o/master
- 让 foo 跟踪 o/master :
🌰:
👇
git checkout -b side o/master
git commit
git pull --rabase
git push
4. Git push 的参数(1)
git push <remote> <place>
<place>
参数是什么意思呢?🌰:git push origin master
把这个命令翻译过来就是:切到本地仓库中的“master”分支,获取所有的提交,再到远程仓库“origin”中找到“master”分支,将远程仓库中没有的提交记录都添加上去,搞定之后告诉我。
我们通过“place”参数来告诉 Git 提交记录来自于 master,要推送到远程仓库中的 master。它实际就是要同步的两个仓库的位置。
5. Git push 的参数(2)
如果来源和去向分支的名称不同呢?比如你想把本地的 foo 分支推送到远程仓库中的 bar 分支。
要同时为源和目的地指定 <place>
的话,只需要用冒号 :
将二者连起来就可以了:git push origin <source>:<destination>
如果你要推送到的目的分支不存在,Git 会在远程仓库中根据你提供的名称帮你创建这个分支!
git push origin foo^:master
🌰:
👇
git push origin master^:foo
git push origin foo:master
6. Git fetch 的参数
git fetch
的参数和 git push
极其相似。他们的概念是相同的,只是方向相反罢了。
<place>
参数
如果你 git fetch origin foo
这样为 git fetch
设置 <place>
的话,Git 会到远程仓库的 foo
分支上,然后获取所有本地不存在的提交,放到本地的 o/foo
上。
指定 <source>:<destination>
—— source
现在指的是远程仓库中的位置,而 <destination>
才是要放置提交的本地仓库的位置。
如果执行命令前目标分支不存在,跟 git push 一样,Git 会在 fetch 前自己创建立本地分支。
git fetch origin foo~1:bar
如果 git fetch
没有参数,它会下载所有的提交记录到各个远程分支……
7. 没有 source 的 source
古怪的 <source>
Git 有两种关于 <source>
的用法是比较诡异的,即你可以在 git push
或 git fetch
时不指定任何 source
,方法就是仅保留冒号和 destination 部分,source 部分留空。
- 如果 push 空
<source>
到远程仓库,会删除远程仓库中的分支:git push origin :side
(本地和远程 side 分支都被删除了) - 如果 fetch 空
<source>
到本地,会在本地创建一个新分支:git fetch origin :bugFix
8. Git Pull 的参数
git pull 实际上就是 fetch + merge 的缩写,git pull 唯一关注的是提交最终合并到哪里(也就是为 git fetch 所提供的 destination 参数)。
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 origin bar:foo
git pull origin master:side