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 命令:
工作中有时需要将一个分支(branch),标签(tag)或其他间接引用解析成相应的commit哈希时。此时你需要使用
git rev-parse 命令。以下命令执行后将显示主分支当前commit的哈希。
这在编写接受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匹配符合的引用。你应该对引用缩写足够熟悉,以便在你每次通过其来切换分支。
上面命令的 some-feature 参数实际上就是分支的缩写。在使用前Git会将其解析为
refs/heads/some-feature 。你也可以使用引用的全名:
git show refs/heads/some-feature |
这样写能避免引用位置产生歧义。这是很必要的,例如,你有标签与分支都叫做
some-feature 然而,当你使用正确的命名规范,标签与分支间的歧义将不再困扰你。
在 Refspecs 部分,我们将看到更多的全名引用。
Packed Refs
对于大型仓库,Git将会周期性地运行垃圾回收将移除不必需要的对象,并将引用压缩至单个文件中,来提高性能。你可以执行下面命令来强制启动这一过程:
这将把在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的祖父级:
但是,当用于合并提交时,事情变的有点复杂。因为合并提交存在一个以上的父级,意味着至少有两条路径可以选择。对于3路合并(两条分支合并为一体),第一父级在你执行合并命令时所在的分支,第二父级在你传入
git merge 命令的那个分支上。
~ 字符将在第一父级上追踪,如果你想要在别的父级上追踪,你需要使用 ^
字符来指定对那一个父级进行追踪。例如,如果你合并提交,下面的命令会追踪第二父级:
可以使用多个 ^ 来移动多代。例如,下面代码展示了追踪第二父级的HEAD的祖父级(假设其为一个合并)
为了说明 ~ 和 ^ 是如何工作的,下图展示了基于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 之前的状态。
这将把你的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 作为参数。
|