原文地址
Git是一切关于commit的艺术:你暂存commit,提交commit,浏览以往的commit,在不同的仓库切换commit,这一切使用不同的命令来实现。这些命令中大部分以各种形式操作commit,一些可以接受commit作为参数。例如,你可以使用git checkout命令来查看以往的commit,只需要传入该commit的哈希即可,抑或传入分支名在不同分支间切换。
通过理解这些使用commit的不同方式,将使得这些命令变得更加强大。本章,我将通过探究commit引用的多种方式来阐述常见命令的内部工作原理,这些常见命令包括git checkout
, git branch
和git push
。
我们也将学到怎样去恢复看似“丢失”的命令,通过Git的reflog机制来访问到它们。
哈希
引用commit最直接的方式就是通过它的SHA-1哈希。这是每个commit独一无二的ID。在git log
的输出中你可以找到每个commit的哈希。
commit 0c708fdec272bc4446c6cabea4f0022c2b616eba
Author: Mary Johnson <mary@example.com>
Date: Wed Jul 9 16:37:42 2014 -0500
Some commit message
当你向其他命令传commit时,你只需要输入足够的字符来标明这个独一无二的提交即可(译注:即你不需要将40位的哈希都输入)例如,你可以查看某个commit通过像下面这样运行git show
命令:
git show 0c708f
工作中有时需要将一个分支(branch),标签(tag)或其他间接引用解析成相应的commit哈希时。此时你需要使用git rev-parse
命令。以下命令执行后将显示主分支当前commit的哈希。
git rev-parse master
这在编写接受commit引用的自定义脚本时非常有用。你可以使用git rev-parse
命令来使你的输入规范化,而非手动编译你的commit引用。
引用(Refs)
引用(Refs)是一种间接引用commit的方式。它是一种对用户来说更亲和的commit哈希的别名。使Git表示分支与标签的内部机制。
引用被作为一个普通的文本文件保存在.git/refs路径下,where .git is usually called .git。要浏览在你的仓库之中的refs,请访问你的.git/refs路径。你将看到以下结构,结构包含的文件因你仓库中的分支,标签,远程分支而异。
.git/refs/
heads/
master
some-feature
remotes/
origin/
master
tags/
v0.9
heads
目录描述了了在你仓库中所有的本地分支。每一个文件名对应了相应的分支,在文件夹内部的文件中你会看他对应的commit哈希。这个哈希是现在的分支最末端的那个commit的哈希。为了证实这点,你可以在Git所在的根目录,执行下面两段代码:
# Output the contents of `refs/heads/master` file:
cat .git/refs/heads/master
# Inspect the commit at the tip of the `master` branch:
git log -1 master
由cat
命令得到的commit哈希应与git log
得到的哈希一致。
要更改主分支的位置就必须要改到refs/heads/master的内容。同样地,创建一个新的分支就是把commit哈希写入新文件这样简单。这也是为何Git与SVN相比是如此轻量的部分原因。
tag文件夹实际上以同样的方式工作着,只是其中存放的是tag而非分支。remotes文件夹将所有由git remote
命令创建的所有远程分支存储为单独的子目录。在每个子目录中,可以发现被fetch进仓库的对应的远程分支。
规范引用(refs)
当你把引用传给Git命令时,你可以使用引用的全称,也可以使用缩写去让Git匹配符合的引用。你应该对引用缩写足够熟悉,以便在你每次通过其来切换分支。
git show some-feature
上面命令的some-feature
参数实际上就是分支的缩写。在使用前Git会将其解析为refs/heads/some-feature。你也可以使用引用的全名:
git show refs/heads/some-feature
这样写能避免引用位置产生歧义。这是很必要的,例如,你有标签与分支都叫做some-feature然而,当你使用正确的命名规范,标签与分支间的歧义将不再困扰你。
在Refspecs
部分,我们将看到更多的全名引用。
Packed Refs
对于大型仓库,Git将会周期性地运行垃圾回收将移除不必需要的对象,并将引用压缩至单个文件中,来提高性能。你可以执行下面命令来强制启动这一过程:
git gc
这将把在refs文件夹所有单独的分支与标签文件移动到在.git根目录中的一个叫做packed-refs
的文件。如果你打开这个文件,你将会发现commit哈希与引用映射表:
00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature
0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/master
bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9
垃圾回收对于正常的Git功能并不会有任何影响。但是,如果你想知道你的.git/refs文件为什么是空的话,现在你知道答案了。
特殊的引用(Refs)
除了引用目录之外,还有一些特别的引用存在于.git路径的顶部:
HEAD – 当前检出的 commit/branch.
FETCH_HEAD – 最新从远程仓库获取的分支。
ORIG_HEAD – 作为备份指向危险操作前的HEAD。
MERGE_HEAD – 使用
git merge
命令合并进当前分支的提交。CHERRY_PICK_HEAD – 使用
git cherry-pick
命令的提交。
当需要时这些引用会被创建或更新。例如,当执行git pull
命令时,首先会执行git fetch
命令,此时会更新FETCH_HEAD引用,其后执行git merge FETCH_HEAD
命令将获取的分支导入仓库。当然上述这些引用可以像普通引用一样使用,我想你一定使用过HEAD作为参数吧。
由于你仓库的类型与状态的差异,这些文件会包含不同的内容。HEAD引用有可能是一个指向其他引用的象征性的引用,也可能是一个commit哈希。当你在主分支下,查看你的HEAD文件内容:
git checkout master
cat .git/HEAD
你将看到ref: refs/heads/master
,这意味着HEAD指向refs/heads/master的引用。这就是为什么Git能获悉当前主分支被检出了的原因。如果切换到其他分支,HEAD的内容将被更新为指向那个分支。但是如果你在commit的层面使用check out
而非分支层面,HEAD的内容将会是一个commit哈希而非引用。这就是为什么Git能获悉它处在独立的状态的原因。
多数情况,HEAD仅仅是一个你可以直接使用的引用。其他仅仅在使用Git内部工作的底层脚本时才会用到。
Refspecs
每个refspec都会创建一个本地仓库分支到远程仓库分支的映射。这让通过本地Git命令操作远程分支成为可能,并且配置一些高级的git push
与git fetch
行为。
refspec被表示为[+]<src>:<dst>。<src>参数表示本地仓库的分支,<src>参数表示远程仓库的目标分支,可选参数+表示是否让远程仓库执行non-fast-forward
更新。
Refspec可与git push
命令联合使用来为远程分支添加不同的名字。例如,以下命令推送主分支到远程分支与寻常git push
命令无二,所不同的是使用了qa-master作为分支名。这样的做法常用于需要将自己的分支推送到远程仓库的QA团队中。
git push origin master:refs/heads/qa-master
你也可以通过refspecs来删除远程分支。在使用特性分支工作流的团队里,将特性分支推送到远程仓库是一个很常见的场景(例如出于备份的目的)。远程特性分支在本地分支从仓库中删除后会依旧存在于远程仓库中,这意味着随着你项目的推进死分支的数量会一直叠加。可以通过以下命令来删除他们:
git push origin :some-feature
这是非常方便的,因为你不需要登录到远程仓库去手动删除远程分支。请注意,在Git v1.7.0你可以使用--delete来替代上述方法。下面的命令具有同样的效果:
git push origin --delete some-feature
通过添加几行代码到Git配置文件中,你可以使用refspec来改变git fetch
命令的行为。通常,git fetch
命令会获取远程仓库所有分支,由于.git/confi文件中的一下部分:
[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch一行告诉git fetch
从源仓库下载所有分支。但是在一些工作流中,你并不需要把他们都下载下来。例如,许多持续集成的工作流只关注主分支。为了只获取主分支,可将fetch行修改为:
[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/master:refs/remotes/origin/master
你可以用相同的方式来配置git push
。例如你总是想要将本地的qa-master推送至远程(像前问所述),你可以按下述方式修改配置文件:
[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/master:refs/remotes/origin/master
push = refs/heads/master:refs/heads/qa-master
Refspecs提供了各种能在仓库间转移分支的Git命令的一个全面控制。有了这些命令你可以重命名或删除本地仓库中的分支,通过别名提交/获取分支,控制git push
和git fetch
命令作用于你指定的分支。
相对引用
你可以通过~
字符来引用相对于另一个commit的commit。例如:下面的代码引用了HEAD的祖父级:
git show HEAD~2
但是,当用于合并提交时,事情变的有点复杂。因为合并提交存在一个以上的父级,意味着至少有两条路径可以选择。对于3路合并(两条分支合并为一体),第一父级在你执行合并命令时所在的分支,第二父级在你传入git merge
命令的那个分支上。
~
字符将在第一父级上追踪,如果你想要在别的父级上追踪,你需要使用^
字符来指定对那一个父级进行追踪。例如,如果你合并提交,下面的命令会追踪第二父级:
git show HEAD^2
可以使用多个^
来移动多代。例如,下面代码展示了追踪第二父级的HEAD的祖父级(假设其为一个合并)
git show HEAD^2^1
为了说明~
和^
是如何工作的,下图展示了基于A通过相对引用如何追踪的每个具体的引用。在一些情况下可以通过多种方式来得到同一个提交:
使用普通引用的命令也能使用相对引用。例如,以下的命令:
# 列出合并提交第二父级上的提交(commits)
git log HEAD^2
# 从当前分支上移除最近三次提交
git reset HEAD~3
# 在当前分支上动态rebase最近三次提交
git rebase -i HEAD~3
Reflog
reflog是Git的安全网,其中记录了基本上所有的本地仓库中的改变,不论你是否提交了快照。你可以把它想象成你对本地仓库做的多有操作的历史记录。可以运行git reflog
命令查看reflog。将会输出如下结果:
400e4b7 HEAD@{0}: checkout: moving from master to HEAD~2
0e25143 HEAD@{1}: commit (amend): Integrate some awesome feature into `master`
00f5425 HEAD@{2}: commit (merge): Merge branch ';feature';
ad8621a HEAD@{3}: commit: Finish the feature
上面代码可解读为:
执行checked out HEAD~2
在此之前,修改了提交信息
在此之前,将特性分支合并进主分支
在此之前,提交了快照
通过HEAD{<n>}语法你可以引用存在reflog中的提交。这与之前章节的HEAD~<n>有着相似的用法,但<n>引用reflog中的记录而不是commit历史中的记录。
你可以使用此方法回滚在别的记录中丢失的状态。例如,刚用git reset
删除一个特性后,你的reflog会像下面这样:
ad8621a HEAD@{0}: reset: moving to HEAD~3
298eb9f HEAD@{1}: commit: Some other commit message
bbe9012 HEAD@{2}: commit: Continue the feature
9cb79fa HEAD@{3}: commit: Start a new feature
在git reset
命令之前执行的三个操作现在处在悬空状态,这意味着若非使用reflog你将无法通过任何方法找到他们的引用。现在你知道你不应该丢掉你所有的工作了吧。你现在需要做的就是检出HEAD@{1}提交,将你的仓库退回到执行git reset
之前的状态。
git checkout HEAD@{1}
这将把你的HEAD分离出来(和分支)从这步你可以创建一个新的分支继续你的特性开发工作。
小结
你现在应该很愉快地引用一个Git仓库中的commit。 我们学习了如何将分支和标签存储为.git子目录中的refs,如何读取packed-refs文件,如何表示HEAD,如何使用refspec进行高级push
和fetch
,以及如何使用相对〜和^字符在分支结构中切换。
我们还了解了reflog,这是一种引用通过任何其他方式不可用的commit的方式。这一个你有种“起死回生”之感的操作。
所有这一切的要点是能够精确地在开发方案中挑选出你的需要的commit。运用本文学到的知识对你已有的Git知识体系将有很大的提升:即对常用的命令git log
,git show
,git checkout
,git reset
,git revert
,git rebase
等命令使用refs
作为参数。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。