要资料 文章 文库 视频 Code iProcess 课程 认证 咨询 工具 讲座吧   专家招募  
会员   
 
  
每天15篇文章
不仅获得谋生技能
更可以追随信仰
 
 
     
   
 订阅
  捐助
Git系列之Refs 与 Reflog
 
来源:www.open-open.com发布于 2017-1-18
 

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^1

可以使用多个 ^ 来移动多代。例如,下面代码展示了追踪第二父级的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 作为参数。

 

相关文章

每日构建解决方案
如何制定有效的配置管理流程
配置管理主要活动及实现方法
构建管理入门
相关文档

配置管理流程
配置管理白皮书
CM09_C配置管理标准
使用SVN进行版本控制
相关课程

配置管理实践
配置管理方法、工具与应用
多层次集成配置管理
产品发布管理
 
分享到
 
 
 

软件配置管理的问题、目的
软件配置管理规范
CQWeb 7.1性能测试与调优指南
为什么需要使用ClearCase
ClearCase与RTC的集成
利用ClearQuest 进行测试管理
更多...   

产品发布管理
配置管理方法、实践、工具
多层次集成配置管理
使用CC与CQ进行项目实践
CVS与配置管理
Subversion管理员

相关咨询服务
SCM启动咨询
SCM流程规范咨询
SCM评估性咨询

配置管理实践(从组织级到项目级)
通号院 配置管理规范与应用
配置管理日构建及持续集成
丹佛斯 ClearCase与配置管理
中国移动 软件配置管理
中国银行 软件配置管理
天津华翼蓝天科技 配置管理与Pvcs
 
 
 
 
 
每天2个文档/视频
扫描微信二维码订阅
订阅技术月刊
获得每月300个技术资源
 
 

关于我们 | 联系我们 | 京ICP备10020922号 京公海网安备110108001071号