接上文Git 之术与道 -- 对象,现在,我们的 dota-game
项目中已经有了下面这些对象:
每个对象都有一个 SHA-1 校验和(40位)。我们可以直接通过这个校验和来索引对象。不过,谁会喜欢记忆这些超长的哈希值呢?
分支
在大多数版本控制系统中,创建新的分支意味着对现有代码执行一次物理拷贝。项目的规模越大,时间开销也就越大。而在 Git 中,分支的创建与合并是非常轻量级的操作。所谓分支,仅仅是一个指向最近一次提交的指针而已。
默认的分支叫做 master 分支。master 这个名字并没有什么特殊之处,你当然可以改作其他名字。分支信息被存储在 .git/refs/heads/
目录下面,文件内容就是分支所指向的 commit 对象:
$ ls .git/refs/heads/
master
$ cat .git/refs/heads/master
c5cbfa0f491087c575d8856632451f8d8763b94f
现在,新建另一个名为 develop 的分支,并提交一些新的内容:
$ git checkout -b develop
$ echo -n "print 'The answer to life the universe and everything is 42.'" > answer.py
$ git add answer.py
$ git commit -m "third commit"
$
$ ls .git/refs/heads/
develop master
$ cat .git/refs/heads/develop
d23dd7b9d38b5560ef5cd8cb3b3b7744a29d808c
分支是 Git 的杀手级特性。你应该在工作流中广泛使用分支,例如:开发分支和发布分支应该区分开来;新功能应该在独立的特性分支上开发等等。不过,太方便了也可能会带来问题,稍不注意,你的代码库就会变得分支众多,版本混乱。
HEAD
Git 怎么知道我们当前在什么分支上呢?.git/HEAD
这个文件记录了当前分支的信息:
$ cat .git/HEAD
ref: refs/heads/develop
HEAD
关键字可以直接拿过来使用,它代表了最近的一次提交:
$ git log HEAD // 等效于 git log d23dd7
$ git show HEAD // 等效于 git show d23dd7
标签
这篇文章已经讲过,Git 中有两种标签,一种标签会在 .git/objects
目录下面创建一个实实在在的对象(注解标签),另一种标签仅仅在 .git/refs/tags
目录中创建一个文件而已(轻量级标签)。我们前面创建的两个标签在项目中的位置如下图所示:
与分支不同,标签的指向是死的,一经创建,它就永远指向同一个地方。
引用日志
HEAD reflogs
执行命令 git reflog
,你会看到类似这种格式的信息:
$ git reflog
d23dd7b HEAD@{0}: checkout: moving from master to develop
c5cbfa0 HEAD@{1}: checkout: moving from develop to master
d23dd7b HEAD@{2}: checkout: moving from master to develop
c5cbfa0 HEAD@{3}: checkout: moving from develop to master
d23dd7b HEAD@{4}: commit: third commit
c5cbfa0 HEAD@{5}: checkout: moving from master to develop
c5cbfa0 HEAD@{6}: commit: second commit
2bafd8d HEAD@{7}: commit (initial): first commit
也许你已经猜到,reflog 记录的是 HEAD
的变更历史。HEAD
的每一次变化,都会在 Git 中留下足迹,这样才不会迷失。
不小心执行了 git reset
?没关系,完全可以轻松撤销回之前的状态:
git reset HEAD@{1}
分支 reflogs
除了 HEAD reflog,每一个分支也有自己的 reflog。分支 reflog 记录了分支的演进历史:
$ git reflog show master
c5cbfa0 master@{0}: commit: second commit
2bafd8d master@{1}: commit (initial): first commit
$
$ git reflog show develop
d23dd7b develop@{0}: commit: third commit
c5cbfa0 develop@{1}: branch: Created from HEAD
时间 reflogs
比如,我想查看一下昨天提交了哪些更新,可以使用这个命令:
$ git diff @{yesterday}
下面所列的时间格式都是可用的(注意单复数):
yesterday
1.minute.ago
2.hours.ago
3.days.ago
4.weeks.ago
1.month.ago
2.years.ago
2015-09-11.23:00:00
祖先引用(Ancestry References)
分支的合并会导致新的 commit 对象有多个父级对象。Git 会把形如 d23dd7^
的索引解析为该对象的“直接父级对象”(也就是与该对象处在相同分支的父对象)。d23dd7^2
则表示该对象的“第二父级对象”(也就是被合并分支上的父对象)。
在 dota-game
项目中执行下列操作:
$ git checkout master // 切换到主分支
$ echo -n "print 'Maybe PHP is not the best language in the universe.'" > main.py
$ git add .
$ git commit -m "fourth commit" // 新建提交
$ git merge develop // 合并 develop 分支
通过索引 34a775^
可以拿到 d7afc1
这个对象;通过索引 34a775^2
可以拿到 d23dd7
这个对象。
$ git show 34a775^
commit d7afc18b714fc0e6d7086c1284d0f2bf1e958d37
fourth commit
...
$ git show 34a775^2
commit d23dd7b9d38b5560ef5cd8cb3b3b7744a29d808c
third commit
...
另一个容易与此混淆的符号是 ~
,~
符号沿着当前分支一直向前回溯。。索引 34a775~
同样拿到 d7afc1
这个对象,因此可以说 34a775~
等价于 34a775^
。不同的是,索引 34a775~2
拿到的是 c5cbfa
对象。Git 会把 ~2
解析为“父级对象的父级对象”(也就是爷爷级对象)。
$ git show 34a775~
commit d7afc18b714fc0e6d7086c1284d0f2bf1e958d37
fourth commit
...
$ git show 34a775~2
second commit
...
这里不太容易理解,千万不要混淆哦。
图示:
区间(Commit Ranges)
多重索引
Git 允许同时查看多个分支上的提交历史,你可以在命令中指明多个分支:
$ git log master develop
排除
你也许想看一下那些在 develop 分支上而不在 master 分支上的提交,这在合并分支的时候非常有用。可以使用 ^
符号指明需要排除的分支:
$ git log ^master develop
因为这个功能在合并分支的时候会被频繁地用到,所以 Git 提供了专门的“双点”(..
)符号:
$ git log master..develop
这和上一条命令是等价的,都会摘选出那些在 develop 分支上而不在 master 分支上的提交。
互斥
有些提交只在 develop 分支上,有些提交只在 master 分支上。“三点”(...)符号帮助你选出那些在其中某一条分支上而不同时在两条分支上的提交:
$ git log master...develop
总结
- 在 Git 中,分支的管理十分便利且强大;
-
HEAD
指示了当前所在的分支; - 与分支不同,标签相当于一个永久性的路桩,一旦被创建,它就会始终指向同一个对象;
- 引用日志提供了一种灵活的方式,当我们需要非线性地访问提交历史时非常有用;
-
^
与~
符号帮助你引用到当前对象的祖先; - 管理分支的时候,在命令中显式地指明提交区间,可以有选择地获取提交历史。