基础篇
循序渐进地介绍 Git 主要命令
Git Commit
Git 仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!
Git 希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。
Git 还保存了提交的历史记录。这也是为什么大多数提交记录的上面都有父节点的原因 —— 我们会在图示中用箭头来表示这种关系。对于项目组的成员来说,维护提交历史对大家都有好处。
Git Branch
- Git 的分支也非常轻量。它们只是简单地指向某个提交纪录 —— 仅此而已。所以许多 Git 爱好者传颂:
1
>* 早建分支!多用分支!
这是因为即使创建再多的分支也不会造成储存或内存上的开销,并且按逻辑分解工作到不同的分支要比维护那些特别臃肿的分支简单多了。
在将分支和提交记录结合起来后,我们会看到两者如何协作。现在只要记住使用分支其实就相当于在说:“我想基于这个提交以及它所有的父提交进行新的工作。”
1
2
3git branch newImage
git checkout newImage
git checkout -b newImage # 创建并切换到分支Git Merge
太好了! 我们已经知道如何提交以及如何使用分支了。接下来咱们看看如何将两个分支合并到一起。就是说我们新建一个分支,在其上开发某个新功能,开发完成后再合并回主线。
咱们先来看一下第一种方法 ——
git merge
。在 Git 中合并两个分支时会产生一个特殊的提交记录,它有两个父节点。翻译成自然语言相当于:“我要把这两个父节点本身及它们所有的祖先都包含进来。”
1
git merge bugFix
Git Rebase
- 第二种合并分支的方法是
git rebase
。Rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。 - Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。
1
git rebase main
注意,提交记录 C3 依然存在(树上那个半透明的节点),而 C3’ 是我们 Rebase 到 main 分支上的 C3 的副本。
1
git rebase bugFix
好了!由于
bugFix
继承自main
,所以 Git 只是简单的把main
分支的引用向前移动了一下而已。- 第二种合并分支的方法是
高级篇
要开始介绍 Git 的超棒特性了,快来吧!
分离HEAD
- HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。
- HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。
- HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。
如果想看 HEAD 指向,可以通过
cat .git/HEAD
查看, 如果 HEAD 指向的是一个引用,还可以用git symbolic-ref HEAD
查看它的指向。分离的 HEAD 就是让其指向了某个具体的提交记录而不是分支名。在命令执行之前的状态如下所示
HEAD -> main -> C1: HEAD 指向 main, main 指向 C1
git checkout C1
现在变成了: HEAD -> C1
相对引用(^)
通过指定提交记录哈希值的方式在 Git 中移动不太方便。在实际应用时,并没有像本程序中这么漂亮的可视化提交树供你参考,所以你就不得不用
git log
来查查看提交记录的哈希值。- 使用
^
向上移动 1 个提交记录,main^
相当于main
的父节点
1
git checkout c3;git checkout HEAD^
- 使用
相对引用2(~)
使用
~<num>
向上移动多个提交记录,如~3
1
git checkout HEAD~4
强制修改分支位置
我使用相对引用最多的就是移动分支。可以直接使用
-f
选项让分支指向另一个提交。例如:1
>git branch -f main HEAD~3
上面的命令会将 main 分支强制指向 HEAD 的第 3 级父提交。
撤销变更
- 在 Git 里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。我们这个应用主要关注的是后者。
- 主要有两种方法用来撤销变更 —— 一是
git reset
,还有就是git revert
。接下来咱们逐个进行讲解。
git reset
git reset
通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset
向上移动分支,原来指向的提交记录就跟从来没有提交过一样。1
git reset HEAD~1
在reset后,
C2
所做的变更还在,但是处于未加入暂存区状态。git revert
- 虽然在你的本地分支中使用
git reset
很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦! - 为了撤销更改并分享给别人,我们需要使用
git revert
1
git revert HEAD
- 奇怪!在我们要撤销的提交记录后面居然多了一个新提交!这是因为新提交记录
C2'
引入了更改 —— 这些更改刚好是用来撤销C2
这个提交的。也就是说C2'
的状态与C1
是相同的。 - revert 之后就可以把你的更改推送到远程仓库与别人分享啦。
- 记得revert之后再
git branch -f pushed HEAD
- 虽然在你的本地分支中使用
移动提交记录
自由修改提交树
Git Cherry-pick
git cherry-pick <提交号>...
如果你想将一些提交复制到当前所在的位置(
HEAD
)下面的话, Cherry-pick 是最直接的方式了。1
git cherry-pick C2 C4
我们只需要提交记录
C2
和C4
,所以 Git 就将被它们抓过来放到当前分支下了。 就是这么简单!交互式 rebase
- 当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 —— 没有比这更简单的方式了。
- 但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase —— 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了
交互式 rebase 指的是使用带参数
--interactive
的 rebase 命令, 简写为-i
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
当 rebase UI界面打开时, 你能做3件事:
- 调整提交记录的顺序(通过鼠标拖放来完成)
- 删除你不想要的提交(通过切换
pick
的状态来完成,关闭就意味着你不想要这个提交记录) - 合并提交。 它允许你把多个提交记录合并成一个。
1
git rebase -i HEAD~4
杂项
Git 技术、技巧与贴士大集合
只取一个提交记录
git rebase -i
git cherry-pick
以便排除那些调试提交
提交的技巧 #1
提交的技巧 #2
Git Tag
Git Describe
高级话题
只为真正的勇士!
- 多次 Rebase
- 两个父节点
- 纠缠不清的分支
Push & Pull —— Git 远程仓库!
是时候分享你的代码了,让编码变得社交化吧
Git Clone
在本地创建一个远程仓库的拷贝(比如从 github.com)
远程分支
- 你可能注意到的第一个事就是在我们的本地仓库多了一个名为
o/main
的分支, 这种类型的分支就叫远程分支。由于远程分支的特性导致其拥有一些特殊属性。 - 远程分支反映了远程仓库(在你上次和它通信时)的状态。这会有助于你理解本地的工作与公共工作的差别 —— 这是你与别人分享工作成果前至关重要的一步.
- 远程分支有一个特别的属性,在你检出时自动进入分离 HEAD 状态。Git 这么做是出于不能直接在这些分支上进行操作的原因, 你必须在别的地方完成你的工作, (更新了远程分支之后)再用远程分享你的工作成果。
为什么有
o/
?你可能想问这些远程分支的前面的
o/
是什么意思呢?好吧, 远程分支有一个命名规范 —— 它们的格式是:<remote name>/<branch name>
因此,如果你看到一个名为
o/main
的分支,那么这个分支就叫main
,远程仓库的名称就是o
。大多数的开发人员会将它们主要的远程仓库命名为
origin
,并不是o
。这是因为当你用git clone
某个仓库时,Git 已经帮你把远程仓库的名称设置为origin
了不过
origin
对于我们的 UI 来说太长了,因此不得不使用简写o
:) 但是要记住, 当你使用真正的 Git 时, 你的远程仓库默认为origin
!如果检出远程分支会怎么样呢?
1
git checkout o/main;git commit
正如你所见,Git 变成了分离 HEAD 状态,当添加新的提交时
o/main
也不会更新。这是因为o/main
只有在远程仓库中相应的分支更新了以后才会更新。关卡答案:
1
2git commit
git checkout o/main;git commit- 你可能注意到的第一个事就是在我们的本地仓库多了一个名为
Git Fetch
当我们从远程仓库获取数据时, 远程分支也会更新以反映最新的远程仓库
git fetch 做了些什么?
git fetch
完成了仅有的但是很重要的两步:- 从远程仓库下载本地仓库中缺失的提交记录
- 更新远程分支指针(如
o/main
)
git fetch
实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。git fetch 不会做的事
git fetch
并不会改变你本地仓库的状态。它不会更新你的main
分支,也不会修改你磁盘上的文件。理解这一点很重要,因为许多开发人员误以为执行了
git fetch
以后,他们本地仓库就与远程仓库同步了。它可能已经将进行这一操作所需的所有数据都下载了下来,但是并没有修改你本地的文件。我们在后面的课程中将会讲解能完成该操作的命令 :D所以, 你可以将
git fetch
的理解为单纯的下载操作。
Git Pull
其实有很多方法的 —— 当远程分支中有新的提交时,你可以像合并本地分支那样来合并远程分支。也就是说就是你可以执行以下命令:
git cherry-pick o/main
git rebase o/main
git merge o/main
- 等等
实际上,由于先抓取更新再合并到本地分支这个流程很常用,因此 Git 提供了一个专门的命令来完成这两个操作。它就是我们要讲的
git pull
。git pull
可以看作git fetch
和git merge
的缩写!
Git Push
注意 ——
git push
不带任何参数时的行为与 Git 的一个名为push.default
的配置有关。它的默认值取决于你正使用的 Git 的版本git push
远程仓库接收了
C2
,远程仓库中的main
分支也被更新到指向C2
了,我们的远程分支 (o/main) 也同样被更新了。所有的分支都同步了!
偏离的提交历史
假设你周一克隆了一个仓库,然后开始研发某个新功能。到周五时,你新功能开发测试完毕,可以发布了。但是 —— 天啊!你的同事这周写了一堆代码,还改了许多你的功能中使用的 API,这些变动会导致你新开发的功能变得不可用。但是他们已经将那些提交推送到远程仓库了,因此你的工作就变成了基于项目旧版的代码,与远程仓库最新的代码不匹配了。
这种情况下,
git push
就不知道该如何操作了。如果你执行git push
,Git 应该让远程仓库回到星期一那天的状态吗?还是直接在新代码的基础上添加你的代码,亦或由于你的提交已经过时而直接忽略你的提交?因为这情况(历史偏离)有许多的不确定性,Git 是不会允许你
push
变更的。实际上它会强制你先合并远程最新的代码,然后才能分享你的工作。git push
看见了吧?什么都没有变,因为命令失败了!
git push
失败是因为你最新提交的C3
基于远程分支中的C1
。而远程仓库中该分支已经更新到C2
了,所以 Git 拒绝了你的推送请求。1
2
3
4
5
6
7可以利用:
git fetch;git rebase o/main;git push
或
git fetch;git merge o/main;git push
建议使用:
git pull --rebase(fetch 和 rebase 的简写);git push
锁定的Main(Locked Main)
如果你是在一个大的合作团队中工作, 很可能是main被锁定了, 需要一些Pull Request流程来合并修改。如果你直接提交(commit)到本地main, 然后试图推送(push)修改, 你将会收到这样类似的信息:
1
>! [远程服务器拒绝] main -> main (TF402455: 不允许推送(push)这个分支; 你必须使用pull request来更新这个分支.)
解决办法
新建一个分支feature, 推送到远程服务器. 然后reset你的main分支和远程服务器保持一致, 否则下次你pull并且他人的提交和你冲突的时候就会有问题.
关卡答案:
1
2
3git checkout -b feature
git push
git branch -f main HEAD~1
关于 origin 和它的周边 —— Git 远程仓库高级操作
做一名仁慈的独裁者一定会很有趣……
- 推送主分支
- 合并远程仓库
- 远程追踪
- Git push 的参数
- Git push 的参数 2
- Git fetch 的参数
- 没有 source 的 source
- Git pull 的参数