Git系列之Refs 与 Reflog

这是上帝的杰作

原文地址
Git是一切关于commit的艺术:你暂存commit,提交commit,浏览以往的commit,在不同的仓库切换commit,这一切使用不同的命令来实现。这些命令中大部分以各种形式操作commit,一些可以接受commit作为参数。例如,你可以使用git checkout命令来查看以往的commit,只需要传入该commit的哈希即可,抑或传入分支名在不同分支间切换。
图片描述
通过理解这些使用commit的不同方式,将使得这些命令变得更加强大。本章,我将通过探究commit引用的多种方式来阐述常见命令的内部工作原理,这些常见命令包括git checkout, git branchgit 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 pushgit 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 pushgit 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进行高级pushfetch,以及如何使用相对^字符在分支结构中切换。

我们还了解了reflog,这是一种引用通过任何其他方式不可用的commit的方式。这一个你有种“起死回生”之感的操作。

所有这一切的要点是能够精确地在开发方案中挑选出你的需要的commit。运用本文学到的知识对你已有的Git知识体系将有很大的提升:即对常用的命令git log,git show,git checkout,git reset,git revert,git rebase等命令使用refs作为参数。

阅读 11.8k

Know little more JS and Technology
一本离散的学习笔记

//loading...

2.2k 声望
159 粉丝
0 条评论

//loading...

2.2k 声望
159 粉丝
文章目录
宣传栏