#
Git主要用来管理提交的:你准备提交,创建提交,查看旧的提交,并且使用其他Git命令来在仓库间迁移提交。大多数这些命令都是在某种形式上用来操作一个提交的,这些命令一般接受提交引用作为他们的参数,比如,你可以通过传入提交的哈希值来git checkout查看一个旧的提交,或者你可以传入一个分支名在分支间切换。
有很多的方式方法来找到某个提交,理解这点会让这些明亮更强大。在这篇文章,我们将通过研究找到某个提交的各种方法来揭示git checkout,git branch以及git push这些常用命令的内部工作机制。
我们将学习通过Git的reflog方法来访问“丢失”的提交来重新找到会他们。
哈希值(Hashes)
最直接访问一个提交的方法就是通过它的SHA-1哈希值了,它是每个提交的唯一ID,你可以使用git log的输出找到所有的提交的哈希值。
commit 0c708fdec272bc4446c6cabea4f0022c2b616eba
Author: Mary Johnson <mary@example.com>
Date: Wed Jul 9 16:37:42 2014 -0500
Some commit message
当你需要把这个提交作为参数传递给其他Git命令时,你仅仅需要确定足够识别唯一代表这个提交的的字符数就行,比如,你想了解上面这个提交的详细信息,你可以使用如下命令:
git show 0c708f
有时候我们需要提交的哈希值来处理一个分支、标签或者另外一个非简介的引用。这个时候,你可以使用git rev-parse命令,下面这个命令返回指向master分支的哈希值:
git rev-parse master
对于写一些需要接受提交引用作为参数的自定义脚本这非常有用。你可以使用git rev-parse来自动生成提交引用,而不是手动解析出提交引用。
Refs
一个ref间接指向一个提交。你可以把它看做是提交哈希值的别名,它对用户来说可读性更友好。在Git内部实现机制里它代表分支或者标签。
Refs采用一般的文本文件存放在.git/refs目录。可以进入.git/refs目录,来深入研究你仓库的refs。你会看到如下结构,它会因为你仓库拥有什么分支、标签、远程仓库而包含不同的文件:
.git/refs/
heads/
master
some-feature
remotes/
origin/
master
tags/
v0.9
heads目录定义了仓库所有的本地分支。每个文件对应一个相关的分支,在文件内你可以找到一个提交哈希值。这个提交哈希值代表分支所在的末端位置。为了验证所说,可以试着执行如下两个命令:
# 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命令返回的哈希值应该是和git log命令返回的提交ID的值是一样的。
为了修改master分支的位置,在Git中可以仅仅通过修改refs/heads/master即可。类似的,创建一个分支也可以简单的在这个目录添加一个新文件,并在文件中写入一个提交哈希值就可以了。这部分解释了相比SVN,Git为什么如此轻量级。
tags目录也是如此,但是它包含的是标签而不是分支。remotes目录则列出了你使用git remote命令创建的远程仓库的子目录,在每个目录中,你将找到所有你获取(fetch)到本地仓库的远程分支。
Specifying Refs
当传入一个ref引用给Git命令时,你可以使用ref的全名,也可以使用短名称,然后让Git去查找一个匹配的ref。你应当已经非常熟悉refs的短名了,因为你每次都会使用的分支名称。
git show some-feature
some-feature参数实际上就是分支的短名,Git会在使用它之前自动指向refs/heads/some-feature,你也可以在命令行明确写出完整的ref,像这样:
git show refs/heads/some-feature
加上ref所在位置可以避免出现歧义。有时候这是必要的,比如,如果你有个分支和标签都叫做some-feature,那是用短名就会存在歧义了。不过,如果你遵守正确的命名传统,标签和分支的歧义通畅不会发生。
我们在Refspecs章节会更多的介绍ref完整命名。
Packed Refs
对于大型仓库,为了性能Git会定期执行垃圾回收来移除不必要的对象并压缩refs到一个文件中。你可以使用如下垃圾回收命令强制执行压缩:
git gc
这会将refs文件夹下的每个分支和标签文件移到.git目录下的packed-refs文件中。如果打开这个文件,你将会看到提交哈希值和refs的对应关系表:
00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature
0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/master
bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9
在外部看来,Git功能的通畅不会受到任何影响,但是,如果你想知道为什么.git/refs为什么是空的,这就是原因。
特殊引用(Special Refs)
作为refs目录的补充,在.git目录会有一些特殊的refs,如下列表所示:
- HEAD – 当前切换到的分支或者提交。
- FETCH_HEAD – 从远程仓库获取到的最新分支。
- ORIG_HEAD – 在严重修改前HEAD的备份。
- MERGE_HEAD – 使用git merge命令合并进当前分支的提交。
- CHERRY_PICK_HEAD – 你执行cherry-pick的提交。
这鞋refs都是由Git创建和更新的。比如,git pull 命令首先会执行git fetch,这就会更新FETCH_HEAD引用,然后它会执行git merge FETCH_HEAD来完成拉取获取到的分支到分支到仓库。当然你可以像其他ref一样使用所有这些ref,和我们处理HEAD一样。
这些文件会根据他们不同的类型已经不同状态而包含不同的内容。HEAD要么包含一个符号链接的ref,它指向另外一个ref而不是一个提交哈希值,或者就是包含一个提交哈希值。比如,如果你在master分支上是看看HEAD的内容吧:
git checkout master
cat .git/HEAD
内容将会是ref: refs/heads/master,这意味着HEAD指向refs/heads/masterref,Git根据这个知道它当前切换到的是master分支。如果你切换到另外一个分支,HEAD的内容会更新内容来对应新的分支。但是,如果你切换到是一个非分支的提交时,HEAD的内容则是包含一个提交的哈希值而不是一个符号链接ref,这样Git就知道它处于分离HEAD状态。
大多数时候,HEAD仅仅代表你直接使用的引用,其他的通畅在写底层的脚本时候有用,比如hook到Git内部去修改Git的一些默认工作。
Refspecs
refspec是本地仓库一个分支到远程仓库分支的关系表。这让我们可以使用本地Git命令来管理远程分支,来配置一些高级命令如git push 和git fetch的行为。
refspec使用[+]
Refspecs可以通过给以不同名称的远程分支来和git push命令一起使用。比如,下面的命令会推送master分支到远程仓库,像一般的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
通过在配置文件中添加几行命令,你能够使用refspecs来修改git fetch的行为。默认git fetch会获取远程仓库的所有分支,原因是因为.git/config文件的配置导致:
[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch那一行高速git fetch会从origin仓库下载所有的分支。举例,许多持续集成工作流仅仅关注master分支。为了仅仅获取master分支,我们可以修改fetch那一行内容如下:
[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/master:refs/remotes/origin/master
你也可以为git push 做相似的配置修改。举例,如果希望总是推送master到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命令在仓库之间如何转移。它们让你可以从本地删除远程分支,重命名远程分支,使用不同的名称fetch/push分支,配置git fetch和git push来选择你想工作的分支。
相关引用(Relative Refs)
你也可以查找一个提交有有关系的提交。~字符让你可以查找父提交,比如,下面这个命令可以查找HEAD的祖父:
git show HEAD~2
But, when working with merge commits, things get a little more complicated. Since merge commits have more than one parent, there is more than one path that you can follow. For 3-way merges, the first parent is from the branch that you were on when you performed the merge, and the second parent is from the branch that you passed to the git merge command.
但是,当有合并提交时,这个情况就有点复杂。因为合并提交会有多个父提交,你有不止一个查找路径。在3-way合并中,第一个父提交是当你执行合并所在分支,第二个父提交是你传给git merge命令的分支。
~字符总是会查找到合并提交的第一个父提交,如果你想查找另一个父提交,你需要使用^字符来明确指出来,比如,如果HEAD是一个合并提交,下面这个命令会找到HEAD的第二个父提交:
git show HEAD^2
你可以使用多个^字符字符来移动不止一代的提交,举例,下面个这命令会查找HEAD(假设它是个合并提交)第二个父提交路径上的祖父提交.
git show HEAD^2^1
可以使用下图来帮助我们搞清楚~ 和 ^的工作原理,图中介绍了如何从A这个commit查找其他相关提交引用,在某些情况下,有许多方法可以找到一个提交。
相关refs可以结合其他命令一起使用,就像一般的引用中的一样。比如,所有下面的命令可以用在相关引用中:
# Only list commits that are parent of the second parent of a merge commit
git log HEAD^2
# Remove the last 3 commits from the current branch
git reset HEAD~3
# Interactively rebase the last 3 commits on the current branch
git rebase -i HEAD~3
The 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
他们的含义如下:
- 你刚刚从master切换到 HEAD~2
- 在此之前你修改了提交信息
- 在此之前你合并了功能分支到master
- 提交了一个功能
- The HEAD{
} syntax lets you reference commits stored in the reflog. It works a lot like the HEAD~ references from the previous section, but the refers to an entry in the reflog instead of the commit history.
You can use this to revert to a state that would otherwise be lost. For example, lets say you just scrapped a new feature with git reset. Your reflog might look something like this:
你可以使用这个命令来回滚到已经丢失的状态,比如,你使用了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目录的,如何读取packed-refs文件的,HEAD的作用,如何在进阶的推送和获取命令中使用refspecs。如果使用相关的~和^操作符号来横跨一个分支结构。
我们也介绍了reflog,它是引用提交的一种方式,这种方式其他手段实现不了。它是后悔药。当你做了不该做的操作时,可以使用它来恢复。
在任何开发场景中所有这些能精确挑出你想要的提交。从本文中学到的技巧可以帮助你使用好Git,比如常见的命令都需要refs作为参数,包括,git log , git show, git checkout, git reset, git revert, git rebase等等。