求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
 订阅
  捐助
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进行版本控制
相关课程

配置管理实践
配置管理方法、工具与应用
多层次集成配置管理
产品发布管理
 
分享到
 
 
最新课程计划
信息架构建模(基于UML+EA)3-21[北京]
软件架构设计师 3-21[北京]
图数据库与知识图谱 3-25[北京]
业务架构设计 4-11[北京]
SysML和EA系统设计与建模 4-22[北京]
DoDAF规范、模型与实例 5-23[北京]

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

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

配置管理实践(从组织级到项目级)
通号院 配置管理规范与应用
配置管理日构建及持续集成
丹佛斯 ClearCase与配置管理
中国移动 软件配置管理
中国银行 软件配置管理
天津华翼蓝天科技 配置管理与Pvcs