如 Git 般思考

0 前言

本文翻译自 Sam Livingston-Gray 的英文博客 Think Like (a) Git

本文的翻译已征得原文作者同意,并且遵循 Creative Commons 相关协议

CC协议的中文版见 知识共享大陆版项目

欢迎大家转载,请同样遵守Creative Commons 相关协议

1 关于本网站

如果你对某些章节的知识已经了如指掌,请直接跳过。
(此部分略去关于原网站本身的一些使用技巧,如两种阅读模式、使用N和P进行翻页的快捷键等,由于与原网站密切相关,而无关网站核心知识,故略去。——译者注)

1.1 本网站适用人群

Git 的“高级初学者”:知道如何适用 Git 创建版本库、向版本库添加(add)和提交(commit)文件、何种情况下使用分支。

如果你现在还不清楚版本控制软件(VCS)是啥玩意,本网站暂时对你用处不大。墙裂建议你去别的地儿转转,或者去我列出的 资源列表页 中瞅瞅,学习基础知识之后再回本网站。

同时,如果你能熟练使用git rebase --interactive,那也不要在这浪费时间了(希望你能使用本网站的框架来帮助其他初学者)。而且,希望你能去 资源列表页 看看,最好推荐几个曾经对你学习 Git 有所帮助的网站链接。

1.2 本网站想教你什么

我的目标是,用一种简单、安全的方法,教授你 Git 某些很疯(实)狂(用)的用法。

为了达到这个目标,你首先要知道一点图论的知识。别被“图论”两个字吓到。最后你会发现,其实“图论”只是一个工具,能让你更好的理某些概念。

介绍完图论,我将说明如何将图论运用到 Git 中,然后跟大家分享“Git 顿悟”。

最后,我将讲解我现在使用 Git 进行版本库管理的方法。


1.3 为什么选择这个网站

我从2008年开始使用 Git 。用 Git 的第一年,基本上算是瞎摆弄,并不十分理解自己到底在干嘛。直到某天,我感觉到 Git 让我的工作更加轻松。于是,我开始总结Git 的使用 方法,以此提高我对使用 Git 某些“危险”特性的理解和信心。

我试着去帮助其他人学习使用 Git ,但并非每次都成功。之后我的一个朋友告诉我,我尽对他说些诸如“所有的 Git 命令其实都是操作图的命令”的话,然而对当时的他并没有什么卵用。

在2011年4月,我尝试着将我所知道的 Git 知识整理成1小时长的讲座。

我在一次茶话会上向同事们做了这个讲座。鉴于讲座的反映非常好,我分别在 Portland BarCamp 2011 和 Cascadia RubyConf 2011 上再次做了演讲。我的演讲稿在 Heroku 上可以找到。

(当时的 视频录像 在这可以找到,但实际上我并不知道我会在现场演讲——当时的演讲者结束的太快,于是会议组织方问:有哪位被拒的演讲申请者有准备好的幻灯片可用?我举了手,于是3分钟后我就上台了。)

这几场演讲的效果都非常好,所以我想把这个讲座制作成系列视频。但是我觉得把它写出来可能更简单而且更容易使用。(我个人挺喜欢看视频的,但是同时观看视频很耗时间。)如果看完这个网站的东东后你依然想看视频,请告诉我。我的联系方式者下一节。

1.4 关于作者

这个网站由我, Sam Livingston-Gray, 开发。我是美国俄勒冈州波特兰市的一个软件工程师,我的邮箱是 geeksam@gmail.com

更多信息请移步 我的个人网站 ,以及 GitHubLinkedInTwitter


2 当你知道……后 Git 就好理解了

正如我在 “为什么选择这个网站?” 一节中说的,我曾经试图通过说一些并卵用的话——他事后告诉我的——来帮助他学习 Git 。通过得到的反馈,我发现了下面这个规律:

人们常常这样说:“当你理解……后你git就好理解”。

所以在2011年4月但是时候我用这句话做关键词 Google 了一下,有8百万个搜索结果。(你可以 自己试试)。

下面我选了几个例子简要讲解一下。

2.1 例1: Kent Beck

Kent Beck,简介。——译者注)


这里写图片描述

这个例子很有代表性。这条推文很深刻,但是除非你知道作者在说什么,否则对你来说并卵用。

我喜欢这条推文,其实还有一个原因。我购买并阅读了 Kent 的好几本书,认为在技术方面他应该无所不知。当我看到这条推文,我意识到我先于他知道了一些东西,这让我很惊讶,这让我又惊又喜。

2.2 例2:Git,仅供4岁及以上儿童使用

Git:仅供4岁及以上儿童使用 (OSDC, 2010)中,Michael Schwern1 使用 Tinker Toys2 来讲解 Git 的工作原理。


Tinker Toys

这是一个伟大的开发者做的一场伟大的报告,如果有时间,墙裂建议你看下他的演讲视频。看下他讲解中的第一句话(加粗的斜体部分):

当你理解 Git 的工作原理时,你就知道为什么 Git 那么做了,因为 Git 其实就只干两件事情。

Tinker Toys 是我发现的 Git 版本库的最好例子。一步一步输入 Git 命令行,我们就一步一步的建立起一个版本库,就跟孩子们根据当前情况决定下一步如何拼接零件一样。像远程分支、rebase、暂存区这些令人脑洞大开的概念都变成了孩子手中的玩具。

警告:小零件存在窒息危险!不适合4岁以下儿童使用!

2.3 例3:HOMEOMORPHIC ENDOFUNCTORS




别担心,这只是个玩笑。
(应该是玩笑吧?是的。)

2.4 例4:LSD和电锯狂人

这个例子不符合“如果你理解……那么……”的句式,但是我觉得它很好的展示了 Git 强大而又奇怪的特点,必须深思才能获得顿悟:

git rebase --interactive 就跟 git commit --amend 一样,像是一个窜来窜去的电锯狂人——完全丧失理智而且相当危险,但是却能震撼心灵、启迪智慧。

——Ryan Tomayko, Git 那些事儿, 2008年4月

2.5 再来一条推文

我正要离开去工作的时候,突然发现了这条推文,赶紧记录下来:


当你意识到think-like-a-git.net是面向 Git 的使用者而非小白用户的时候,你就能更好的理解它了

好了,现在回到 Git 的教学环节。


3 图论

【如果你已经熟悉了图论知识,可以跳过该部分直接阅读“可达性”一节。】

关于图论更深一层的数学背景,请看 维基百科 。下面是维基百科上一个简短的定义:

图是由若干给定的点以及连接两点的线所构成的图形。

图论能够用来描述很多事物,我以一个最直接的例子入手:地图。你可以把图论当成是一个对信息进行编码的工具,编码的对象包括地图的两个方面:某个地点和去该地点的路线。

咱们从最基础的部分开始。

【就跟巧克力、培根和暖气管道这些人类本能需求一样,我觉得图论的产生也是如此的自然而然。要掌握这些知识是要花些时间的,所以,如果你发现阅读该小节时自己昏昏欲睡,别担心。只要“可达性”一节能看懂,你可以再回来阅读这些东西。】

3.1 七桥问题

下面是18世纪初普鲁士哥尼斯堡市的地图。


真是散步的好地方啊

有一条河(蓝色部分)穿过该市,河上有两个小岛,有七座桥(红色部分)把两个岛与河岸联系起来。有个人提出一个问题: 一个步行者怎样才能不重复、不遗漏地一次走完七座桥,最后回到出发点

1735年,一个叫欧拉的数学家证明这样的路线是不存在的。在解决该问题时,他开创了一个数学分支,就是我们现在说的图论。

3.2 地点和路线

下面是哥尼斯堡市地图的两个版本。第一个是你已经看到过的,第二个是欧拉在证明中使用到的版本。


原始版本
欧拉的版本

这里最酷最能激荡人灵魂的地方是: 欧拉剔除了所有跟问题无关的信息

这里写图片描述

如果你关心的仅仅通过哪座桥,那么问题就可以抽象为, 每块了陆地是一个点,每座桥是连接两个点的线。

所以我说,图论可以当做是一门研究地点以及路线的学问。

3.3 节点和边

在前面的章节中,我说过图论可以被简化成地点以及路线的问题。下面是我们之前看到过的一个定义:

图是由若干给定的点以及连接两点的线所构成的图形。

  • 点:某个地点;
  • 线:到达地点所经过的路线

在七桥问题中,陆地和岛都是点,而桥是线。


原始版本
欧拉的版本

3.4 节点标记

相对于数学家只关心图的形状,程序员和计算机科学家更关心使用图论解决实际问题。

鉴于此,给节点打上标记是一个很有用的办法。以美丽的波特兰市的公共交通系统为例,我们有公交车轻轨电车通勤火车有轨电车甚至缆车


波特兰市的轻轨电车轨道:10000英尺:1的比例

如果此时你举起小手满脸羞红地问:额,为什么每条线的颜色不同?
那么,恭喜你朋友,你上路了。

3.5 给线打上标记

就像给点打上标记一样,我们也可以给线打上标记,如:

  • 名称:例如,第12大道;
  • 数字(也叫权重):比如距离或速度值;
  • 类型:表明关系,如“朋友”或“同事”或“父子”;
  • 方向:在前面的例子中,像”朋友“、“同事”这样的关系都是双向的,但是“父子”这种就只能是单向的(除非,你是Robert A. Heinlein小说中的人物)。

    社交网络。天使们,赶紧给我5000w刀风投资金!

    其实,最后一点,方向是很重要的,我们将单独开辟一节来讲。

3.6 有向图 vs 无向图


这里写图片描述
无向图(没有箭头)

我们用很关键的一个属性来区分各种图:有向和无向。当讨论“无向图”的时候,我脑海中会出现一幅画面:一张地铁线路图漫无目的地坐在板凳上,一脸茫然,它的父母在一旁唉声叹气,问它什么时候才能像个大人一样承担起责任,做点对生活有意义的事情……当然了,也许只有我才会这么想。其实,更确切的说,是双向还是非双向。

有向图
有向图(有箭头)

多数我见过的图只有一种类型的边,只有少数的图包含两种类型的——例如,街道的地图,上面可能有无向边表示双行道,而用有向边表示单行道——这是我能想到的唯一一个例子。

3.7 可达性

下面这个图,你可以把它想象成并行的三个宇宙空间,时间从左往右飞逝,即A是历史的起点。(箭头表示“跟随于”或“从属于”的关系,故你可以说 “B 跟着 A”。)


这里写图片描述

敲入 git rebase --help 查看该图的 ASCII 版本

E 出发,你会看到的历史是A,B,C,D,E

H 出发,你会看到的历史是A,B,F,G,H

B 出发,你会看到的历史是A,B,C,I,J,K

其实,这张图的重点是,无论你从哪个节点出发,你永远无法到达图的某些部分。听起来很让人沮丧,那我们换个说法:根据你所处的起点的不同,你只能到达图的一部分,而另一部分无法到达

听起来不是很重要?咱们接着往下走,你就不这么认为了。


4 图和 Git

之所以到现在为止讲了这么多关于图论的东东,是因为 Git 版本库本身就是一张巨大的图。


这里写图片描述
Git commits (简化版)

你用 Git 做的大多数操作其实都是提交(commit),只是形式不同罢了。表面上,Git 的提交(commit)操作包含两个东西:(1)一个指向代码当前状态的指针;(2)0个或多个指向“父提交”(”parent” commits)操作的指针;

提示:“指针这个词意味着你在使用图论的东西”

ps:如果你想了解更多,墙裂推荐你去看 Scott Chacon 写的 PDF “Git 内部原理”(链接无效——译者注)。或者,去看他的演讲,他对 Git 的理解简直让人叹为观止。


4.1 可视化你的 Git 版本库

我一般通过命令行使用 Git 。但是,当我想清楚的知道每一步都发生了什么时,我会使用 Git 可视化工具。在 Mac 上,我觉得最好用的事 GitX 。在其他操作系统上,gitx 也应该有相应的版本,你也可以在网上搜索一下,找到适合自己的工具。

可视化工具的主要作用是帮你理解分支的变化过程。例如,在命令行中列出所有的 commit 操作,你可以敲入 git log --oneline --abbrev-commit --all ——会得到如下图的结果。

(感谢 @cflipse 指出 --pretty=oneline --branches= 可以简写为 --oneline --all !)*


这里写图片描述
很乱,是不是?

使用参数 --graph ,可以让结果稍微清楚些:

git log --oneline --abbrev-commit -all --graph


这里写图片描述
看起来顺眼多了,有木有?

(感谢 @mjdominus@JRGarcia 指出 --pretty=oneline --branches= 可以简写为 --oneline --all !)*

如果你想看到分支和标签,加上 --decorate 参数:

git log --oneline --abbrev-commit --all --graph --decorate


这里写图片描述
图太宽了,我得换个网页模板了…

(感谢 @JRGarcia 提出的 --decorate 的建议!)

你还可以加上 --color 参数,这样显示会更清楚(不好意思,这次没来得及截图):

git log --oneline --abbrev-commit --all --graph --decorate --color

实际上,我设置了一个快捷键来完成这个操作:

alias gg='git log --oneline --abbrev-commit -all --graph --decorate --color'

但是大多数时候,我是使用 GitX 的,它会以一种更加清晰明了的图形界面来系显示:


这里写图片描述
我好喜欢死这些五颜六色了

4.2 引用

你已经看到上一节中 GitX 截图中五颜六色的标记了。(没有看到?回到上一节去看看,我等你。)这些标记是 GitX 表现引用的方式。

我不愿花过多时间去解释每种颜色代表什么意思,因为这可能是 GitX 专有的,其他软件或许有自己的规定。但是不管你的可视化工具的各种颜色是啥意思,你都需要知道。(此处应有掌声。)

引用是指向提交操作的指针

引用会在以下几种情形中出现:本地分支,远程分支,标签

在磁盘上,一个本地分支引用就是 ./git/refs/heads 目录下的一个文件。这个文件的内容是一个40字节(在Mac OS X 中应为41字节——译者注)大小的某个提交(commit)操作的 ID,而该本地分支引用也正是指向该提交(commit)操作的。该文件的大小是40个字节。

你也许听别人说过 Git “轻量级的创建分支”。他们说的就是(或者部分是)这个意思。用 Git 创建一个分支仅仅是在磁盘上写一个40字节大小的文件,所以执行 git branch foo 命令才快的令人发指。

然而,引用真正有趣的地方是它的工作原理。咱们继续探讨这个。

4.3 指针的指针

之前说过,引用有几种不同的形式,都是指向版本库的提交(commit)操作。不同形式的引用,唯一不同的是它们移动的方式和时机。(当我说指针移动时,我是说提交(commit)操作的 ID 值改变了。)

本地分支引用专属于你的本地版本库。影响本地分支引用的操作包括 commitmerge, rebase, reset

远程分支引用专属于事先定义好的远程版本库。影响远程分支的操作包括 fetchpush

标签引用基本上可以理解为不会移动的分支引用。一旦创建了一个标签,它就不会改变了(除非你明确的使用 --forece 选项更新它)。标签引用的这个特性用在为软件包打版本号或用来标记软件部署的时间戳。迄今为止,我只知道一个可以影响标签的命令,也许你已经猜到了——就是 tag 命令本身。

4.4 理解 GitX 的图形界面

再来看下 GitX 的界面截图,上面有一些注释。


这里写图片描述
如果你想搞一个版本库把玩一下,可以 克隆这个

然而,重点并不在这张图上,而是你看不到的部分。我见过的 Git 可视化工具有个共同点:它们不让你看见提交(commit)操作

听起来像个阴谋?其实不然。

4.5 垃圾回收

假如:你写了一些代码,也检查过了。突然,你发现你忘记跑测试用例了,跑了之后你发现代码有语法错误。或者不小心多按了字符。不管出于什么原因,实际上你并没有完成。(这种情况对我简直是家常便饭)

如果用 Subversion,在这情况下,你唯一能做的就是每次改动都进行一次提交(commit)操作。通常,我的版本库里会有三个或四个版本。第一个版本可能是“增加功能 X ”,下面几个版本可能是“哦, 多打了个字符”或“修复bug”或“忘记跑测试用例了”。

Git 给你另一个选择:你可以使用 git commit --amend 命令将新的改动添加到上一个提交(commit)操作中。这个命令使你能够将相关的一组改动捆绑在同一个提交(commit)操作中,以便你回头查看时更容易理解。

关于 Git commit 操作另一个有趣的知识点是:一个 commit 操作的 ID 是 一个SHA-1 哈希值,哈希的对象包括 commit 的内容和该 commit 的父 commit 们的的 ID 值

也就是说,当你使用 git commit -amend 的时候,你是在创建一个完全不同的 commit (节点),而且在将本地分支引用指向这个节点。前一个 commit 操作仍然在磁盘上,你还可以回去(稍后再讨论这个问题)。然而,不管是 git log 还是可是话工具都不会将前一个 commit 显示给你看。

最终,Git 决定是时候进行 垃圾回收 了。(你可以使用 git gc 命令自行触发这一操作。) Git 将会从每个分支和每个标签开始,回溯遍历整个图,建立一个列表,列出每个 commit 节点能到达的所有节点。当所有的路线都遍历完成后, Git 会删除那些没有被访问到的节点。


5 实战训练

上一节有些知识点很重要,重要的事要多说几遍。

在描述 Git 的垃圾回收算法的时候,我说过,“从每个分支和每个标签开始, Git 回溯遍历整个图,建立一个列表,列出每个 commit 节点能到达的所有节点。”(重点已加粗)

到目前为止,我写的这些东西都是为了让你理解这一点。如果我只有十秒钟告诉你 Git 的终极奥秘,即本网站的重中之重,我可以用下面这句话概括:
引用使提交操作可达

5.1 引用使提交操作可达

我们将这个句子分解下:
引用…
……不管是本地分支,远程分支,还是标签……
使提交操作
……即图中的节点……
可达
……以便你能回溯遍历到该节点,而且在垃圾回收的时候不会被清除掉……

我花了相当长一段时间才理解上述几点。我希望把这些写下来,希望你们别像曾经的我一样浪费时间。

5.2 简单的开始

当我开始使用 Git 时,我特别担心丢失代码。在这之前,我用过的唯一一款 VCS 是 Subversion ,创建分支特别复杂难懂而且极容易出现各种问题。所以我当时并没有花时间去练习使用 merge 操作——更别说 rebase 操作了,大家都说 rebase 很危险。

于是,当我尝试某些我不确定的东西时,我先备份整个目录

$ cd ..
cp -r work backup_work
$ cd work

然后我开始做 merge,rebase, 或其他我认为很复杂的操作。如果结果是对的,我就继续;否则,我就删除工作目录,重命名备份目录,重新尝试。

虽然没有做记录,但是我敢肯定第一年一整年的时间我都在这么做。

当我最后意识到引用使提交操作可达的时候,我觉得当时自己备份整个版本库的做法简直太愚蠢了,而且总结出了一套使版本库恢复到某个特定状态的方法。

5.3 分支即存档

由于 Git 的分支只是磁盘上一个40字节的文件,所以计算机完成该操作所花的时间,要远小于你敲入 git branch foo 命令所用的时间,甚至小好几个数量级。

而且因为分支就是引用,(和我一起读)引用使提交操作可达,创建一个分支其实是钉住图的某个部分,以便之后还能够回来。

由于 git mergegit rebase 操作都会改变现有的提交操作(记住,提交操作的 ID 值是它的内容和历史提交操作的哈希值),当你对你要尝试的东西不太确定时,你可以随时创建一个临时分支。

换句话说,创建一个临时分支就像你打游戏时挑战Boss之前的存档操作。

5.4 最简单的开始

几乎每次写程序我都会使用 GitX 。GitX 有一点很像浏览器:版本库发生变化后它不会立即主动刷新——你要按下 Cmd+R 告诉它刷新。


闪开闪开,我要做实验了
这件T恤不错吧,我知道你想要一件

你可以充分利用这一点。这么做:当你在命令行中完成了某个操作,去看下可视化工具,它还没有立即刷新。此时,尝试着去预测一下可视化工具的界面会发生怎样的变化。(你甚至可以自己在纸上画出来。)然后,手动刷新界面,观察下实际的变化跟你的预期一致吗?

if (一致) {
    恭喜!你学到东西了!
} else {
    恭喜!发现进步的梯子了!
}

不断地重复上述过程,慢慢的你就预测的越来越准。

5.5 测试 merge 操作

现在,我对分支操作已经轻车熟路。但是刚开始的时候,当我尝试使用不同的分支来开发不同的功能的时候,merge 操作使我相当头疼。如果现在的你跟我当时一样,下面有两种简单的方法,让你尝试着使用 git merge 操作,直到你彻底理解 merge 操作的含义。

I’ve put together two slight variations on the same operation. Both techniques basically do the same thing, but one of them relies on a slightly scarier-sounding Git command for the undo.

根据你所做操作的不确定性来决定到底选哪个方案——既跟你的 Git 技能水平有关,也跟你要进行 merge 操作有关。

  • 炮灰模式,如果你对 git merge 命令不甚明白,或者你觉得你肯定会恢复 merge 操作之前的状态;
  • 存档模式,如果你对你所要做的事情十分清楚,只是想万一事情搞砸了还有回旋余地。

5.6 炮灰模式

快速的浏览一下该方案的要点:

  1. 确定你处于正确的分支,而且工作目录正常;
  2. 创建一个新的分支(我通常命名为 test_merge),然后转到该分支;
  3. 执行 merge 操作;
  4. 转到可视化工具,预测下接下来会发生什么变化;
  5. 刷新可视化工具,验证你的判断是否正确;
  6. 你对结果满意吗?
    如果满意:Move your real branch forward to where the test_merge branch is.
    如果不满意:删除 test_merge 分支;

我把这个方案称为炮灰方案:你对前面的形势不甚清楚,于是你排除一波侦察兵去查看情况。如果一起正常,你就继续前进并和他们汇合;否则,这只是一小波侦察兵,你告诉他们的家人他们光荣了(成为炮灰了)就行了……

完整方案
你现在 master 分支,你想把 spiffy_new_feature 分支的改动何必到 master 分支。你不确定这个决定是否足够明智,所以你打算尝试一下 merge 操作,但是如果不顺利的话还能及时中止操作。

  1. 确定你处于正确的分支,而且工作目录正常;
  2. 不管你使用什么样的可视化工具,一定要确保你现在出于正确的分支中,在命令行中敲入 git status 你应该会看到如下结果:
# On branch master
nothing to commit (working directory clean)
  1. 创建一个新的分支(我通常命名为 test_merge),然后转到该分支;
    敲入 git checkout -b test_merge 。现在,如果你再次 git status ,你会看到提示信息说你已经在test_merge 分支了。
  2. 执行 merge 操作
    执行 git merge spiffy_new_feature。如果你够幸运,不会有冲突产生的;如果你想中止 merge 操作,执行 git reset --hard 即可;
  3. 转到可视化工具,预测下接下来会发生什么变化;
    例如:
    • merge 操作之后,你会看到一个新的 commit;
    • 新的 commit 会有提示信息,如“Merge branch ‘spiffy_new_feature’ into master”;
    • test_merge 分支应该已经被移动到新的 commit 节点了,而 masterspiffy_new_branch 分支还在原处;
  4. 刷新可视化工具,验证你的判断是否正确
  5. 你对结果满意吗?
    如果满意:合并 mastertest_merge 分支:
git checkout master
git merge teset_merge

如果不满意:删除 test_merge 分支:

git checkout master
git branch -D test_merge

5.7 存档模式

快速浏览下该模式的主要步骤:

  1. 确定你处于正确的分支,而且工作目录正常;
  2. 创建新分支作为存档点,但是别跳转到该新分支;
  3. 执行 merge 操作;
  4. 转到可视化工具,预测下接下来会发生什么变化;
  5. 刷新可视化工具,验证你的判断是否正确;
  6. 对结果是否满意:
    如果满意:删除存档点;
    如果不满意:将分支恢复到存档点;

(如果你玩过超级玛丽,那么你对“存档”这个概念应该相当熟悉。)

完整方案
你现在 master 分支,你想把 spiffy_new_feature 分支的改动何必到 master 分支。你很确定你要保留这些更改,但是如果不顺利的话(例如这个功能有意外的副作用时)还能及时中止操作。

  1. 确定你处于正确的分支,而且工作目录正常;
  2. 不管你使用什么样的可视化工具,一定要确保你现在出于正确的分支中,在命令行中敲入 git status 你应该会看到如下结果:
# On branch master
nothing to commit (working directory clean)
  1. 创建一个新的分支(我通常命名为 test_merge),但是别转到该新分支;
    敲入 git branch savepoint 。现在,如果你再次 git status ,你会看到提示信息说你已经在master 分支了。
  2. 执行 merge 操作
    执行 git merge spiffy_new_feature;如果你想中止 merge 操作,执行 git reset --hard 即可;
  3. 转到可视化工具,预测下接下来会发生什么变化;
    例如:
    • merge 操作之后,你会看到一个新的 commit;
    • 新的 commit 会有提示信息,如“Merge branch ‘spiffy_new_feature’ into master”;
    • master 分支应该已经被移动到新的 commit 节点了,而 spiffy_new_branch 分支还在原处;
  4. 刷新可视化工具,验证你的判断是否正确
  5. 你对结果满意吗?
    如果满意:删除存档点:
    git branch -d savepoint
    如果不满意:将分支恢复到存档点:
    git reset --hard savepoint
    如果你想清理版本库,你现在可以执行 git branch -d savepoint 删除存档点。

5.8 Merge 高级技巧

一旦你熟悉了存档模式,你也许会厌烦于每次新建存档分支然后再删掉它。如果这样,有个好消息告诉你:实际上你并不必使用分支作为存档点。

Merge commits always wind up with a branch label pointing at them, and one of the branch’s parent commits will be the commit that that branch label was just moved from.

It may take you a few tries to parse that sentence. The upshot is that the commit you started on—the one you would’ve marked with a savepoint branch—will always be reachable.

其实是这样的:Git 并不关心你怎么称呼分支。记住:分支其实是磁盘上一个40字节大小的文件,指向提交操作的 SHA-1 哈希值,这个哈希值才是 Git 用来干活的东西。分支只是为了让弱(愚)小(蠢)的人类更容易、更方便的记住图的某一部分。

在存档模式的结尾,我们知道把分支恢复到存档点的命令是 git reset --hard savepoint (这里的 savepoint 是分支名)。如果你去使用 git reset -h 查看说明文档,你会发现该命令的最后一个参数是 <commit> ,早期版本的说明文档称这个参数为 <commit-ish> ,其实是在提醒我们:我们可以用任何能转换成 SHA-1 哈希值的值当做该参数。

Git 很乐意当做 <commit> 参数的值有:

  • 分支名;
  • 标签;
  • 相对引用,如HEAD^, HEAD^^, HEAD~3;
  • 部分 SHA-1 哈希值,如 8d434382(你只要提供足够多能唯一确定某个哈希值的几个字符即可,Git 会自动为你补全剩余的部分);
  • 完整的 SHA-1 哈希值,例如 SHA-1 8d434382d9420940be260199a8a058cf468d9037 (Git 能很容易识别出这是一个SHA-1 哈希值);

So, at the end of the Savepoint pattern, if you wanted to back out of the merge, you could just as easily use git log or your visualizer to find the SHA-1 of the commit (let’s say it starts with 1234abcd), and type this:

所以,在存档模式一节的结尾,如果你想恢复到 merge 之前的状态,你只需简单的使用 git log 或从你的可视化工具中查出 commit 操作的 SHA-1 值(比如说以1234abcd开头),然后敲入:

git reset --hard 1234abcd

…and Git would behave exactly the same as if you’d remembered to create a branch in the first place.

“if you’d remembered to” 让我想起了一篇对我帮助特别大的文章:Ryan Tomayko 写的 Git 那些事儿 (是的,正是我在 LSD and Chainsaws 一节中提到过的那个)。


6 rebase 详解

rebase 命令是花费我最多时间去弄清楚的命令之一。一方面因为我对分支操作的奥义不甚清楚,另一方面 rebase 本身很危险、很诡异(至少大多数人都这么认为)。

这些对我来说,太丢人了,因为 rebase 是我在 Git 中使用到的最强大的命令,更重要的是,一旦你掌握它了,它非常好理解。

DANGER, WILL ROBINSON!
本节,让我们一起揭开 rebase 的盖头来。我打算好好写下试验 rebase 命令的方法,就跟之前的 merge 一样。但是,最近我发现我忘记了一件关于 rebase 非常重要的事: 一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行 rebase 操作。 针对这一点,后面我会详细写的。

注意:迄今为止,我所收到的问题绝大多数是关于 rebase 的。关于 rebase 我也有很多话要说,但是分身乏术,实在抽不出时间来写作。暂时,这篇文章 (译注:链接失效)可以给出一些启发。

言归正传
讲解 rebase 之前,我打算岔会儿题,因为我觉得先讲 Git 的一个命令会有助于 rebase 的理解:git cherry-pick

6.1 浅尝 cherry-pick

在 Git 官网帮助文档中,对 cherry-pick 有一段简短而且牛逼的描述:

Given one or more existing commits, apply the change each one introduces, recording a new commit for each.

我之前说过,一个 commit 操作的 ID 值,是该操作内容和历史的哈希值。所以,及时两个 commit 操作的内外完全一致,但是指向不同的父节点,它们的 ID 值也是不同的。

git cherry-pick 命令所做的事,基本上是把一个分支中的 commit 节点拿来,在你当前的分支中“回放”一遍。虽然“回放”所做的改动相同,但由于父节点不同,所以新生成的 commit 节点拥有不同的 ID 值。

我们再回到可达性一节的例子:


这里写图片描述

假如你在节点 H,然后执行 git cherry-pick E(为简单起见,我们用节点标号 E 来表示该 commit 节点,实际上应该是完整或部分 SHA-1 校验值),之后你会得到节点 E 的副本——即 E’——指向 H 节点,如下图:


这里写图片描述

或者,如果你执行 git cherry-pick C D E,会得到下图的结果:


这里写图片描述

注意,在上述两个命令中,git 复制一个地方的改动,然后将其在另一个地方回放。

下面是一个该过程的详细过程:


stpe1
step1


step2
step2


step3
step3


step4
step4


step5
step5


step6
step6


step7
step7

6.2 使用 ‘git cherry-pick’ 模拟 ‘git rebase’

一旦你理解了 git cherry-pick,你可以把 git rebase 当做是对给定分支上的多有 commit 节点一次性实施 cherry-pick 操作的快捷方式,而不用每次都单独输入某节点的 ID 值。

再次回一下我们的例子,这次我们加些分支:


这里写图片描述

现在,执行这些命令行:

git checkout foo
git checkout -b newbar
git cherry-pick C D E

这些命令依次做了:

  • 确保我们在 H 节点(因为’foo‘指向它);
  • 新建并转到临时分支”newbar”,该分支也指向 H;
  • 在分支”newbar”上应用 C, D, E 节点的改动,新建 C’, D’, E’ 节点,更新”newbar”使其指向E’;

得到的版本库如下图:


这里写图片描述

然后,执行:

git checkout bar 
git reset --hard newbar 
git branch -d newbar

这些命令:

  • 转到分支”bar”;
  • 移动强制指针”bar”使其指向”newbar”所指向的节点(多亏 –hard 选项,更新了我的工作目录);
  • 删除临时分支”newbar”;

    做完这些,我的工作目录就变成这样了(注意,由于没有分支指向原来的节点 D 和 E,故这两个节点已经不可到达):


    这里写图片描述

    其实,上面的整个过程可以用一条命令完成:

git rebase foo bar

换句话说,git rebase 是一个快捷方式,让你把版本库某个部分整体移动到其他地方的快捷方式。

6.3 更好的记住 rebase 的参数

总结一下,以下命令:

git checkout foo
git checkout -b newbar
git cherry-pick C D E 
git checkout bar 
git reset --hard newbar 
git branch -d newbar

等价于:

git rebase foo bar

当你使用 rebase 的时候,其实你在重写历史。实际上,你在对 git 说:“Hey,看到那条时间线的一系列改动没?你就假装它们发生在这条时间线。”

rebase 的帮助文档上列出了一大堆用法。坦白的说,我也不知道其中一些是做什么用的,密密麻麻的的括号让人眼花缭乱,要花好大力气才知道这些选项到底是干什么用的,比如,–onto。

这是一点拙见:
英语的阅读顺序是从左到右。大多数图表中,如果要表示随时间变化,一般时间位于图表的 x 轴,向右代表时间在增长。当你执行 shell 命令的时候,你可以把几个命令放在一行执行,按从左到右的顺序依次执行。

所以当我使用 git rebase 时,我(几乎)总是给它两个选项:起始分支,结束分支。换句话说,我告诉 rebase 它应该完成的一系列动作,从左到右的顺序:git rebase first_this then_this


7 结束语

Where do we go from here?
Where do we go from here?
The battle’s done, and we kind of won
So we sound our victory cheer
Where do we go from here?

–Buffy the Vampire Slayer, “Once More, With Feeling”

严格地说,这并不是结束——只是我目前只写到这里。我真诚的希望你喜欢这篇博客,真诚的希望我帮你学到了点东西。

期待你的反馈!欢迎 邮件我(geeksam@gmail.com) 或在 Twitter(https://twitter.com/#!/geeksam)上关注我。

如果关于 git ,你还有什么想知道的地方,尽管说!之所以有动力写下这么多东西,主要是因为我在三次演讲之后,一直收到感谢信,信上说我的演讲很好的帮助了他们。如果有时间,我很愿意再丰富这个网站的资料,如果你让我知道你在关注这个网站而且告诉我你想看到什么方面的内容,我将更加动力十足。

祝学习愉快!
-Sam


  1. 当今 Perl 语言领域的顶级专家之一,曾编写或维护包括Class::DBI, Test::More and ExtUtils::MakeMaker 在内的 Perl 模块。
  2. 一种通过棍棍和关节拼接组成各种形状的儿童玩具
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值