10_Git之Git提交引用和引用日志


转载:感谢童仲毅同学写了该系列文章,我只是搬运工,谢谢!

这是一篇在原文基础上演绎的译文。除非另行注明,页面上所有内容采用知识共享-署名(CC BY 2.5 AU)协议共享。

提交(commit)是Git的精髓所在,你无时不刻不在创建和缓存提交、查看以前的提交,或者用各种Git命令在仓库间转移你的提交。大多数的命令都对同一个提交操作,而有些会接受提交的引用作为参数。比如,你可以给git checkout 传入一个引用来查看以前的提交,或者传入一个分支名来切换到对于的分支。

引用一次提交的各种方式

知道提交的各种引用方式之后,Git的命令就会变得更加强大。在这章中,我们研究提交的各种引用方式,来一窥git checkoutgit branchgit push等命令的工作原理。

我们还会学到如何使用Git的引用日志查看看似已经删除的提交。

哈希字串

引用一个提交最直接的方式是通过SHA-1的哈希字串,这是每个提交唯一的ID。你可以在git log的输出中找到提交的哈希字串。

1
2
3
4
5
commit 0c708fdec272bc4446c6cabea4f0022c2b616eba
Author: Mary Johnson <mary@example.com>
Date: Wed Jul 9 16:37:42 2014 -0500

Some commit message

在Git命令中传递时,你只需要提供足以确定那个提交的哈希子串即可。比如,你可以这样用git show的命令显示上面的提交:

1
git show 0c708f

有时,我们需要把分支、标签或者其他间接的引用转变成对应提交的哈希。git rev-parse命令正是你需要的。下面这个命令返回master分支提交的哈希字串:

1
git rev-parse master

当你写的自定义脚本中需要将提交引用作为参数时,这个命令非常有用。你可以让git rev-parse帮你处理转换,而不用手动做这件事。

引用

ref是提交的间接引用。你可以把它当做哈希字串的别名,但对用户更友好。这就是Git内部表示分支和标签的机制。

引用以一段普通的文本存在于.git/refs目录中,就是我们平时说的那个.git。你要去.git/refs文件夹查看仓库中的引用。你可以看到下面这样的结构,但具体的文件取决于你的仓库中有什么分支和标签,以及你的远程仓库。

1
2
3
4
5
6
7
8
9
.git/refs/
heads/
master
some-feature
remotes/
origin/
master
tags/
v0.9

heads目录定义了你本地仓库中的所有分支。每一个文件名和你的分支名一一对应,文件中包含一个提交的哈希字串。这个就是分支顶端的所在位置。为了验证这一点,试试在Git根目录运行下面这两个命令:

1
2
3
4
5
6

# 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命令显示的哈希字串应该是一致的。

如果要改变master分支的位置,Git只需要更改refs/heads/master的文件内容。同样地,创建新的分支也只需要将当前提交的哈希字串写入到新的文件中。这也是为什么Git分支比SVN轻量那么多的其中一个原因。

tags目录也是以相同的方式存储,只不过其中存的是标签而不是分支。remotes目录将你之前用git remote命令创建的所有远程仓库以子目录的形式一一列出。在每个文件夹中,你可以找到所有fetch到本地仓库的远程分支。

###指定引用
当你向Git命令传入引用的时候,你既可以指定引用完整的名称,也可以使用缩写,然后让Git来寻找匹配。你应该已经对引用的缩写很熟悉了,每次你通过名称引用分支的时候都会这么做。

1
git show some-feature

这里的some-feature参数其实是分支名的缩写。Git在使用前将它解析成refs/heads/some-feature。你也可以在命令行中指定引用的全称,就像这样:

1
git show refs/heads/some-feature

这避免了引用可能产生的所有歧义。这是非常必要的,比如你同时有一个标签和分支都叫some-feature。然而,如果使用正常的命名规范,你不应该有这样的歧义。

我们会在refspec一节见到更多引用名称。

打包引用目录

对于大型仓库,Git会周期性地执行垃圾回收来移除不需要的对象,将所有引用文件压缩成单个文件来获得更好的性能。你可以使用这个命令强制垃圾回收来执行压缩:

1
git gc

这个命令把refs文件夹中所有单独的分支和标签移动到了.git根目录下的packed-refs文件中。如果你打开这个文件,你会发现提交的哈希字串和引用之间的映射关系:

1
2
3
00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature
0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/master
bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9

另一方面,正常的Git功能不会受到任何影响。但如果你好奇你的.git/refs文件夹为什么是空的,这一节告诉你了答案。

特殊的引用

除了refs文件夹外,.git根目录还有一些特殊的引用。如下所示:

  • HEAD – 当前所在的提交或分支。
  • FETCH_HEAD – 远程仓库中fetch到的最新一次提交。
  • ORIG_HEAD – HEAD的备份引用,避免损坏。
  • MERGE_HEAD – 你通过git merge并入当前分支的引用(们)。
  • CHERRY_PICK_HEAD – 你cherry pick使用的引用。

这些引用s由Git在需要时创建和更新。比如说,git pull命令首先运行git fetch,而FETCH_HEAD引用随之改变。然后,它运行git merge FETCH_HEAD来将fetch到的分支最终并入仓库。当然,你也可以使用其他任何引用,因为我相信你已经对HEAD很熟悉了。

这些文件包含的内容取决于它们的类型和你的仓库状态。HEAD 引用可以包含符号链接(指向另一个引用而不是哈希字串),或是提交的哈希字串。比如说,看看当你在master分支上时HEAD的内容:

1
2
git checkout master
cat .git/HEAD

这个命令会输出ref: refs/heads/master,也就是说HEAD指向refs/heads/master这个引用。这也正是Git如何知道现在所在的是master分支。如果你要切换分支,HEAD的内容将会被更新到新的分支。但如果你要切换到一个提交而不是分支,HEAD会包含一个提交的哈希而不是符号引用。这就是Git如何知道现在HEAD处于分离状态。

在大多数情况下,HEAD是你唯一用得到的引用。其它引用一般只在写底层脚本,接触到Git内部的工作机制时才会用到。

refspec

refspec将本地分支和远程分支对应起来。我们可以通过它用本地的Git命令管理远程分支,设置一些高级的git pushgit fetch行为。

refspec的定义是这样的:[+]<src>:<dst><src>参数是本地的源分支,<dst>是远程的目标分支。可选的+号强制远程仓库采用非快速向前的更新策略。

refspec可以和git push一起使用,用来指定远程的分支的名称。比如,下面这个命令将master分支push到远程origin,就像一般的git push一样,但它使用qa-master作为远程仓库中的分支名。对于QA团队来说,这个方法非常有用。

1
git push origin master:refs/heads/qa-master

你也可以用refspec来删除远程分支。feature分支的工作流经常会遇到这种情况,将feature分支push到远程仓库中(比如说为了备份)。你删除本地的feature分支之后,远程的feature分支依然存在,虽然现在我们已经不再需要它。你可以push一个<src>参数为空的refspec来删除它们,就像这样:

1
git push origin :some-feature

这非常方便,因为你不需要登陆到你的远程仓库然后手动删除这些远程分支。注意,在Git v1.7.0之后你可以用--delete标记代替上面这个方法。下面这个命令和上面的命令作用相同:

1
git push origin --delete some-feature

在Git配置文件中增加几行,你就可以更改git fetch的行为。默认地,git fetch会fetch远程仓库中所有分支。原因就是因为.git/config文件的这段配置:

1
2
3
[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/*:refs/remotes/origin/*

fetch这一行告诉git fetch从origin仓库中下载所有分支。但是,一些工作流不需要所有分支。比如,很多持续集成工作流只关心master分支。为了做到这一点,我们需要将fetch这行改成下面这样:

1
2
3
[remote "origin"]
url = https://git@github.com:mary/example-repo.git
fetch = +refs/heads/master:refs/remotes/origin/master

你还可以类似地修改git push的配置。比如,如果你总是将master分支push到origin仓库的qa-master分支(就像我们之前做的一样),你要把配置文件改成这样:

1
2
3
4
[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

refspec给了你完全的掌控权,可以定制Git命令如何在仓库之间转移分支。你可以重命名或是删除你的本地分支,fetch或是push不同的分支名,修改git pushgit fetch的设置,只对你想要的分支进行操作。

相对引用

你还可以通过提交之间的相对关系来引用。~符号让你访问父节点的提交。比如说,下面这个命令显示HEAD祖父节点的提交:

1
git show HEAD~2

但是,对于合并提交(merge commit)的时候,事情就会变得有些复杂。因为合并提交有多个父节点,所以你可以找到多条回溯的路径。对于3路合并,第一个父节点是你执行merge时的分支,第二个父节点是你传给git merge命令的分支。

~符号总是选择合并提交的第一个父节点。如果你想选择其他父节点,你需要用^符号来指定。比如说,HEAD是一个合并提交,下面这个命令返回HEAD的第二个父节点:

1
git show HEAD^2

你可以使用不止一个^来查看超过一层的节点。比如,下面的命令显示的是HEAD的祖父节点,也就是HEAD第二个父节点的父节点。

1
git show HEAD^2^1

为了阐明~^是如何工作的,下面这张图告诉你如何使用相对引用,来指向任意的提交。有的提交可以通过多种方式引用。

Accessing commits using relative refs

相对引用在命令中的用法和普通的引用相同。比如,下面所有命令中使用的都是相对引用:

1
2
3
4
5
6
7
8
# 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

引用日志

引用日志是Git的安全网。它记录了你在仓库中做的所有更改,不管你有没有提交。你也可以认为这是你本地更改的完整历史记录。运行git reflog命令查看引用日志。它应该会打印出像下面这样的信息:

1
2
3
4
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

说人话就是:

  • 你刚刚切换到HEAD~2
  • 你刚刚修改了一个提交信息
  • 你刚刚把feature分支合并到了master分支
  • 你刚刚提交了一份缓存

HEAD{<n>}语法允许你引用保存在日志中的提交。这和上一节的HEAD~<n>引用差不多,不过<n>指的是引用日志中的对象,而不是提交历史。

你可以用办法回到之前可能已经丢失的状态。比如,你刚刚用git reset方法粉碎了新的feature分支。你的引用日志看上去可能会是这样的:

1
2
3
4
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前的三个提交现在都成了悬挂的了,也就是说除了引用日志之外没有办法再引用到它们。现在,假设你意识到了你不应该丢掉你全部的工作。你只需要切换到HEAD@{1}这个提交就能回到你运行git reset之前仓库的状态。

1
git checkout HEAD@{1}

这会让你处于HEAD分离的状态。你可以从这里开始,创建新的分支,继续你的工作。

总结

你现在对Git提交的引用应该已经相当熟悉了。我们知道了分支和标签是如何存在于.git的子文件夹refs中,如何读取打包的引用文件,如何使用refspec来进行更高级的push和fetch操作,如何使用~^符号来遍历分支结构。

我们还了解了引用日志,来引用到其他方式已经不存在的提交。这是一种很好的恢复误删提交的方法。

它的意义在于:在任何开发场景下,你都能找到你需要的特定提交。你很容易就可以把这些技巧用在你一有的Git知识中,因为很多常用的命令都接受引用作为参数,包括git log,git showgit checkoutgit resetgit revertgit rebase等等。

文章目录
  1. 1. 哈希字串
  2. 2. 引用
  3. 3. 打包引用目录
  4. 4. 特殊的引用
  5. 5. refspec
  6. 6. 相对引用
  7. 7. 引用日志
  8. 8. 总结
|