网址:https://learngitbranching.js.org/?locale=zh_CN
介绍:这个网站可以让我们通过游戏过关的方式来学习 Git,通过动画让我们明白整个执行过程,边学边玩可以说非常有趣了。下面我将每个关卡的一些命令做一些总结帮助记忆和学习 Git(要理解清楚是需要通过自己跟着提示进行命令操作练习才行)。
1. 基础篇(Git 主要命令)
1.1 Git Commit(提交)
创建一个新的提交记录
1.2 Git Branch(分支)
Git 的分支也非常轻量。它们只是简单地指向某个提交纪录 —— 仅此而已。即使创建再多分的支也不会造成储存或内存上的开销,并且按逻辑分解工作到不同的分支要比维护那些特别臃肿的分支简单多了。
git branch newImage
:创建一个名为 newImage 的分支,newImage 的指向是当前的提交记录。
git checkout newImage
:切换到我们创建的 newImage 分支上
git checkout -b 分支名
:创建一个新分支同时切换到这个新分支上
1.3 分支与合并
如何将两个分支合并到一起?(下面两种方法的区别具体可以通过网址上的动画演示过程去体会)
第一种方法:git merge
比如我们创建了一个新的分支并且提交了一次git checkout -b bugFix;git commit
这时候我们再切换到主分支再次进行一次提交git checkout master;git commit
然后我们如何将 master 和 bugFix 两个分支合并呢?可以使用git merge bugFix
第二种方法:git rebase
(实际上就是取出一系列的提交记录,“复制” 它们,然后在另外一个地方逐个的放下去)
Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰 (比如我们将一个新创建的分支 bugFix 合并到 master 主分支上只要用git rebase master
即可)。
2. 高级篇(Git 的超棒特性)
2.1 分离 HEAD
什么是 HEAD?(以下三个解释也是可以通过该网址的动画演示过程去体会其意思)
- HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。
- HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。
- HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。
分离的 HEAD 就是让其指向了某个具体的提交记录而不是分支名。在命令执行之前的状态如下所示:
HEAD -> master -> C1
(HEAD 指向 master, master 指向 C1)
执行了git checkout C1
后就会变成HEAD -> C1
2.2 相对引用 (^ 与~)
该网址中提到通过指定提交记录哈希值的方式在 Git 中移动不太方便。在实际应用时,并没有像本程序中这么漂亮的可视化提交树供你参考,所以你就不得不用git log
来查查看提交记录的哈希值。并且哈希值在真实的 Git 世界中也会更长,例如提交记录的哈希值可能是fed2da64c0efc5293610bdd892f82a58e8cbc5d8
,令人欣慰的是,Git 对哈希的处理很智能。你只需要提供能够唯一标识提交记录的前几个字符即可。因此我可以仅输入fed2
而不是上面的一长串字符。
正如我前面所说,通过哈希值指定提交记录很不方便,所以 Git 引入了相对引用。(使用相对引用的话,你就可以从一个易于记忆的地方(比如bugFix
分支或HEAD
)开始计算)
这里介绍了两个简单的用法:
- 使用
**^**
向上移动 1 个提交记录 - 使用
**~数字**
向上移动多个提交记录,如~3
**示例:**比如现在切换到 master 的父节点我们就可以使用git checkout master^
, 如果想切换到第二个父节点就可以使用git checkout master^^
,当然也可以将 HEAD 作为参照物,比如git checkout HEAD^
使用~
操作符与上不同是当我们需要向上移动很多步的时候就不用敲那么多的^
,可以用~数字
来代替,比如移动到当前 master 的第三个父节点就可以使用git checkout HEAD~3
在这里教程中还提到了强制修改分支位置使用git branch -f 分支名 HEAD~3
,这个命令会将该分支强制指向 HEAD 的第 3 级父提交。
2.3 撤销变更
在 Git 里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。我们这个应用主要关注的是后者。
这里教程中又提到了两种方法来撤销变更:
- 第一种使用
git reset
- 第二种使用
git revert
这里主要也需要通过网站中的动画过程演示来帮助我们理解和记忆
示例:我们先来看第一种 Reset,当我们使用git reset HEAD~1
,就会把分支记录回退 1 个提交记录来实现撤销改动,原来指向的提交记录就跟从来没有提交过一样 (在 reset 后,C2
所做的变更还在,但是处于未加入暂存区状态)。然后说到 Revert,虽然我们在本地分支使用 git reset
很方便,但是这种 “改写历史” 的方法对大家一起使用的远程分支是无效的。为了撤销更改并分享给别人,我们需要使用git revert
3. 移动提交记录(自由修改提交树)
3.1 git cherry-pick(git cherry-pick <提交号>
)
如果你想将一些提交复制到当前所在的位置(HEAD
)下面的话, cherry-pick
是最直接的方式了。
当我们想将一个分支上的工作复制到当前所在分支上,或许你会想到之前的 rebase,但是这里我们可以看看 cherry-pick 的效果,通过git cherry-pick C2 C4
这里就是将 C2 C4 两个提交记录抓到当前分支下。
3.2 交互式的 rebase
当我们知道所需要提交的记录 (并且知道这些提交记录的哈希值) 时,用 cherry-pick 再好不过了,但是如果我们不清楚这些,我们此时也可以用交互式的 rebase— 如果你想从一系列的提交记录中找到想要的记录,这就是最好的方法。交互式 rebase 指的是使用带参数--interactive
的 rebase 命令, 简写为-i
,如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
当 rebase UI 界面打开时, 你能做 3 件事:
- 调整提交记录的顺序(通过鼠标拖放来完成)
- 删除你不想要的提交(通过切换
pick
的状态来完成,关闭就意味着你不想要这个提交记录) - 合并提交。 遗憾的是由于某种逻辑的原因,这个网址不支持此功能,因此没有详细介绍这个操作。简而言之,它允许你把多个提交记录合并成一个。
4. 杂项 (Git 技术、技巧与贴士大集合)
4.1 本地栈式提交
来看一个在开发中经常会遇到的情况:我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。这些调试和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因,解决掉以后觉得沾沾自喜!最后就差把 bugFix
分支里的工作合并回 master
分支了。你可以选择通过 fast-forward 快速合并到 master
分支上,但这样的话 master
分支就会包含我这些调试语句了。你肯定不想这样。
那我们如何做到只提交一个记录呢?
很简单,我们只需要将分支切换到 master 主分支,再通过git rebase -i
或者git cherry-pick
选择我们要提交的记录就能够很容易的来达到目的。
4.2 提交的技巧
你之前在 newImage
分支上进行了一次提交,然后又基于它创建了 caption
分支,然后又提交了一次。此时你想对的某个以前的提交记录进行一些小小的调整。比如设计师想修改一下 newImage
中图片的分辨率,尽管那个提交记录并不是最新的了。
我们可以通过下面的方法来克服困难:
- 先用
git rebase -i
将提交重新排序,然后把我们想要修改的提交记录挪到最前 - 然后用
git commit --amend
来进行一些小修改 - 接着再用
git rebase -i
来将他们调回原来的顺序 - 最后我们把 master 移到修改的最前端(用你自己喜欢的方法),就大功告成啦!
当然完成这个任务的方法不止上面提到的一种(很容易我们就想到了之前的 cherry-pick
也是可以做到的)
4.3 Git Tags
相信通过前面课程的学习你已经发现了:分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。你可能会问了:有没有什么可以_永远_指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
这个时候就有了**git tag**
,它们并不会随着新的提交而移动。你也不能检出到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
我们只需要用git tag v1 提交记录
如git tag v1 C1
表示这是我们 1.0 版本,我们将这个标签命名为v1
,并且明确地让它指向提交记录C1
,如果你不指定提交记录,Git 会用HEAD
所指向的位置。
4.4 Git Describe
由于标签在代码库中起着 “锚点” 的作用,Git 还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),它就是 git describe
!
Git Describe 能帮你在提交历史中移动了多次以后找到方向;当你用 git bisect
(一个查找产生 Bug 的提交记录的指令)找到某个提交记录时,或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令。
使用:git describe <ref>
,<ref>
可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会以你目前所检出的位置(HEAD
)
这里主要通过该网址的动画演示过程去理解
5. 远程仓库
5.1 Git Clone
直到现在, 教程都聚焦于本地仓库的操作(branch、merge、rebase 等等)。但我们现在需要学习远程仓库的操作 —— 我们需要一个配置这种环境的命令, 它就是git clone
。 从技术上来讲,git clone
命令在真实的环境下的作用是在本地创建一个远程仓库的拷贝(比如从 github.com)。
5.2 远程分支 o/master
当我们git clone
后发现一个一个名为o/master
的分支, 这种类型的分支就叫远程分支。由于远程分支的特性导致其拥有一些特殊属性。你可能想问这些远程分支的前面的 o/
是什么意思呢?好吧, 远程分支有一个命名规范 —— 它们的格式是:
<remote name>/<branch name>
因此,如果你看到一个名为 o/master
的分支,那么这个分支就叫 master
,远程仓库的名称就是 o
。
大多数的开发人员会将它们主要的远程仓库命名为 origin
,并不是 o
。这是因为当你用 git clone
某个仓库时,Git 已经帮你把远程仓库的名称设置为 origin
了。不过 origin
对于我们的 UI 来说太长了,因此不得不使用简写 o
:) 但是要记住, 当你使用真正的 Git 时, 你的远程仓库默认为 origin
!
5.3 Git Fetch
Git 远程仓库相当的操作实际可以归纳为两点:向远程仓库传输数据以及从远程仓库获取数据。
如何从远程仓库获取数据 —— 命令如其名,它就是git fetch
。
git fetch
完成了仅有的但是很重要的两步:
- 从远程仓库下载本地仓库中缺失的提交记录
- 更新远程分支指针 (如
o/master
)
git fetch
实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。git fetch
并不会改变你本地仓库的状态。它不会更新你的master
分支,也不会修改你磁盘上的文件。所以, 你可以将git fetch
的理解为单纯的下载操作。
5.4 Git Pull
Git 帮我们将 git fetch
和git merge
合到了一起,让我们直接用一步git pull
就能实现从远程仓库拉取并且合并分支。
5.5 Git Push
上传自己分享内容与下载他人的分享刚好相反,那与 git pull
相反的命令是什么呢?git push
!
git push
负责将你的变更上传到指定的远程仓库,并在远程仓库上合并你的新提交记录。一旦 git push
完成, 你的朋友们就可以从这个远程仓库下载你分享的成果了!
5.6 解决远程库提交历史的偏离
假设你周一克隆了一个仓库,然后开始研发某个新功能。到周五时,你新功能开发测试完毕,可以发布了。但是 —— 天啊!你的同事这周写了一堆代码,还改了许多你的功能中使用的 API,这些变动会导致你新开发的功能变得不可用。但是他们已经将那些提交推送到远程仓库了,因此你的工作就变成了基于项目旧版的代码,与远程仓库最新的代码不匹配了。
这种情况下, git push
就不知道该如何操作了。如果你执行 git push
,Git 应该让远程仓库回到星期一那天的状态吗?还是直接在新代码的基础上添加你的代码,亦或由于你的提交已经过时而直接忽略你的提交?
因为这情况(历史偏离)有许多的不确定性,Git 是不会允许你 push
变更的。实际上它会强制你先合并远程最新的代码,然后才能分享你的工作。
如何解决问题呢?
我们可以通过先git fetch
下载代码, 再通过git rebase o/master
或者git merge o/master
进行合并,合并之后再进行推送git push
,可能有人觉得写得命令太长,之前介绍过git pull
是fetch
和merge
的简写,类似的**git pull --rebase**
就是fetch
和rebase
的简写了。
好了总结到此处,主要还是要自己跟着页面中的教程提示以及动画过程演示去理解操作才能运用的熟练哦!