Git系列之Refs 与 Reflog
DebDTD
8年前
<p>Git是一切关于commit的艺术:你暂存commit,提交commit,浏览以往的commit,在不同的仓库切换commit,这一切使用不同的命令来实现。这些命令中大部分以各种形式操作commit,一些可以接受commit作为参数。例如,你可以使用 <em>git checkout</em> 命令来查看以往的commit,只需要传入该commit的哈希即可,抑或传入分支名在不同分支间切换。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/17414acdf5fc4813f46d236a145613c6.png"></p> <p>通过理解这些使用commit的不同方式,将使得这些命令变得更加强大。本章,我将通过探究commit引用的多种方式来阐述常见命令的内部工作原理,这些常见命令包括 git checkout , git branch 和 git push 。</p> <p>我们也将学到怎样去恢复看似“丢失”的命令,通过Git的reflog机制来访问到它们。</p> <h2>哈希</h2> <p>引用commit最直接的方式就是通过它的SHA-1哈希。这是每个commit独一无二的ID。在 git log 的输出中你可以找到每个commit的哈希。</p> <pre> commit 0c708fdec272bc4446c6cabea4f0022c2b616eba Author: Mary Johnson <mary@example.com> Date: Wed Jul 9 16:37:42 2014 -0500 Some commit message</pre> <p>当你向其他命令传commit时,你只需要输入足够的字符来标明这个独一无二的提交即可(译注:即你不需要将40位的哈希都输入)例如,你可以查看某个commit通过像下面这样运行 git show 命令:</p> <pre> git show 0c708f</pre> <p>工作中有时需要将一个分支(branch),标签(tag)或其他间接引用解析成相应的commit哈希时。此时你需要使用 git rev-parse 命令。以下命令执行后将显示主分支当前commit的哈希。</p> <pre> git rev-parse master</pre> <p>这在编写接受commit引用的自定义脚本时非常有用。你可以使用 git rev-parse 命令来使你的输入规范化,而非手动编译你的commit引用。</p> <h2>引用(Refs)</h2> <p>引用(Refs)是一种间接引用commit的方式。它是一种对用户来说更亲和的commit哈希的别名。使Git表示分支与标签的内部机制。</p> <p>引用被作为一个普通的文本文件保存在 <em>.git/refs</em> 路径下,where .git is usually called .git。要浏览在你的仓库之中的refs,请访问你的 <em>.git/refs</em> 路径。你将看到以下结构,结构包含的文件因你仓库中的分支,标签,远程分支而异。</p> <pre> .git/refs/ heads/ master some-feature remotes/ origin/ master tags/ v0.9</pre> <p>heads 目录描述了了在你仓库中所有的本地分支。每一个文件名对应了相应的分支,在文件夹内部的文件中你会看他对应的commit哈希。这个哈希是现在的分支最末端的那个commit的哈希。为了证实这点,你可以在 <em>Git</em> 所在的根目录,执行下面两段代码:</p> <pre> # 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</pre> <p>由 cat 命令得到的commit哈希应与 git log 得到的哈希一致。</p> <p>要更改主分支的位置就必须要改到 <em>refs/heads/master</em> 的内容。同样地,创建一个新的分支就是把commit哈希写入新文件这样简单。这也是为何Git与SVN相比是如此轻量的部分原因。</p> <p>tag文件夹实际上以同样的方式工作着,只是其中存放的是tag而非分支。remotes文件夹将所有由 git remote 命令创建的所有远程分支存储为单独的子目录。在每个子目录中,可以发现被fetch进仓库的对应的远程分支。</p> <h3>规范引用(refs)</h3> <p>当你把引用传给Git命令时,你可以使用引用的全称,也可以使用缩写去让Git匹配符合的引用。你应该对引用缩写足够熟悉,以便在你每次通过其来切换分支。</p> <pre> git show some-feature</pre> <p>上面命令的 some-feature 参数实际上就是分支的缩写。在使用前Git会将其解析为 <em>refs/heads/some-feature</em> 。你也可以使用引用的全名:</p> <pre> git show refs/heads/some-feature</pre> <p>这样写能避免引用位置产生歧义。这是很必要的,例如,你有标签与分支都叫做 <em>some-feature</em> 然而,当你使用正确的命名规范,标签与分支间的歧义将不再困扰你。</p> <p>在 Refspecs 部分,我们将看到更多的全名引用。</p> <h2>Packed Refs</h2> <p>对于大型仓库,Git将会周期性地运行垃圾回收将移除不必需要的对象,并将引用压缩至单个文件中,来提高性能。你可以执行下面命令来强制启动这一过程:</p> <pre> git gc</pre> <p>这将把在refs文件夹所有单独的分支与标签文件移动到在 <em>.git</em> 根目录中的一个叫做 packed-refs 的文件。如果你打开这个文件,你将会发现commit哈希与引用映射表:</p> <pre> 00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature 0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/master bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9</pre> <p>垃圾回收对于正常的Git功能并不会有任何影响。但是,如果你想知道你的 <em>.git/refs</em> 文件为什么是空的话,现在你知道答案了。</p> <h2>特殊的引用(Refs)</h2> <p>除了引用目录之外,还有一些特别的引用存在于 <em>.git</em> 路径的顶部:</p> <ul> <li> <p>HEAD – 当前检出的 commit/branch.</p> </li> <li> <p>FETCH_HEAD – 最新从远程仓库获取的分支。</p> </li> <li> <p>ORIG_HEAD – 作为备份指向危险操作前的HEAD。</p> </li> <li> <p>MERGE_HEAD – 使用 git merge 命令合并进当前分支的提交。</p> </li> <li> <p>CHERRY_PICK_HEAD – 使用 git cherry-pick 命令的提交。</p> </li> </ul> <p>当需要时这些 <em>引用</em> 会被创建或更新。例如,当执行 git pull 命令时,首先会执行 git fetch 命令,此时会更新 <strong>FETCH_HEAD</strong> 引用,其后执行 git merge FETCH_HEAD 命令将获取的分支导入仓库。当然上述这些引用可以像普通引用一样使用,我想你一定使用过HEAD作为参数吧。</p> <p>由于你仓库的类型与状态的差异,这些文件会包含不同的内容。HEAD引用有可能是一个指向其他引用的象征性的引用,也可能是一个commit哈希。当你在主分支下,查看你的HEAD文件内容:</p> <pre> git checkout master cat .git/HEAD</pre> <p>你将看到 ref: refs/heads/master ,这意味着HEAD指向refs/heads/master的引用。这就是为什么Git能获悉当前主分支被检出了的原因。如果切换到其他分支,HEAD的内容将被更新为指向那个分支。但是如果你在commit的层面使用 check out 而非分支层面,HEAD的内容将会是一个commit哈希而非引用。这就是为什么Git能获悉它处在独立的状态的原因。</p> <p>多数情况,HEAD仅仅是一个你可以直接使用的引用。其他仅仅在使用Git内部工作的底层脚本时才会用到。</p> <h2>Refspecs</h2> <p>每个 <em>refspec</em> 都会创建一个本地仓库分支到远程仓库分支的映射。这让通过本地Git命令操作远程分支成为可能,并且配置一些高级的 git push 与 git fetch 行为。</p> <p><em>refspec</em> 被表示为 <em>[+]<src>:<dst></em> 。 <strong><src></strong> 参数表示本地仓库的分支, <strong><src></strong> 参数表示远程仓库的目标分支,可选参数 <em>+</em> 表示是否让远程仓库执行 non-fast-forward 更新。</p> <p>Refspec可与 git push 命令联合使用来为远程分支添加不同的名字。例如,以下命令推送主分支到远程分支与寻常 git push 命令无二,所不同的是使用了 <em>qa-master</em> 作为分支名。这样的做法常用于需要将自己的分支推送到远程仓库的QA团队中。</p> <pre> git push origin master:refs/heads/qa-master</pre> <p>你也可以通过 <em>refspecs</em> 来删除远程分支。在使用特性分支工作流的团队里,将特性分支推送到远程仓库是一个很常见的场景(例如出于备份的目的)。远程特性分支在本地分支从仓库中删除后会依旧存在于远程仓库中,这意味着随着你项目的推进死分支的数量会一直叠加。可以通过以下命令来删除他们:</p> <pre> git push origin :some-feature</pre> <p>这是非常方便的,因为你不需要登录到远程仓库去手动删除远程分支。请注意,在Git v1.7.0你可以使用 <strong>--delete</strong> 来替代上述方法。下面的命令具有同样的效果:</p> <pre> git push origin --delete some-feature</pre> <p>通过添加几行代码到Git配置文件中,你可以使用refspec来改变 git fetch 命令的行为。通常, git fetch 命令会获取远程仓库所有分支,由于.git/confi文件中的一下部分:</p> <pre> [remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/*:refs/remotes/origin/*</pre> <p><em>fetch</em> 一行告诉 git fetch 从源仓库下载所有分支。但是在一些工作流中,你并不需要把他们都下载下来。例如,许多持续集成的工作流只关注主分支。为了只获取主分支,可将 <em>fetch</em> 行修改为:</p> <pre> [remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/master:refs/remotes/origin/master</pre> <p>你可以用相同的方式来配置 git push 。例如你总是想要将本地的 <em>qa-master</em> 推送至远程(像前问所述),你可以按下述方式修改配置文件:</p> <pre> [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</pre> <p>Refspecs提供了各种能在仓库间转移分支的Git命令的一个全面控制。有了这些命令你可以重命名或删除本地仓库中的分支,通过别名提交/获取分支,控制 git push 和 git fetch 命令作用于你指定的分支。</p> <h2>相对引用</h2> <p>你可以通过 ~ 字符来引用相对于另一个commit的commit。例如:下面的代码引用了HEAD的祖父级:</p> <pre> git show HEAD~2</pre> <p>但是,当用于合并提交时,事情变的有点复杂。因为合并提交存在一个以上的父级,意味着至少有两条路径可以选择。对于3路合并(两条分支合并为一体),第一父级在你执行合并命令时所在的分支,第二父级在你传入 git merge 命令的那个分支上。</p> <p>~ 字符将在第一父级上追踪,如果你想要在别的父级上追踪,你需要使用 ^ 字符来指定对那一个父级进行追踪。例如,如果你合并提交,下面的命令会追踪第二父级:</p> <pre> git show HEAD^2</pre> <p>可以使用多个 ^ 来移动多代。例如,下面代码展示了追踪第二父级的HEAD的祖父级(假设其为一个合并)</p> <pre> git show HEAD^2^1</pre> <p>为了说明 ~ 和 ^ 是如何工作的,下图展示了基于A通过相对引用如何追踪的每个具体的引用。在一些情况下可以通过多种方式来得到同一个提交:</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/05eed2f53d5c0e779f7bd5be21272627.png"></p> <p>使用普通引用的命令也能使用相对引用。例如,以下的命令:</p> <pre> # 列出合并提交第二父级上的提交(commits) git log HEAD^2 # 从当前分支上移除最近三次提交 git reset HEAD~3 # 在当前分支上动态rebase最近三次提交 git rebase -i HEAD~3</pre> <h2>Reflog</h2> <p>reflog是Git的安全网,其中记录了基本上所有的本地仓库中的改变,不论你是否提交了快照。你可以把它想象成你对本地仓库做的多有操作的历史记录。可以运行 git reflog 命令查看reflog。将会输出如下结果:</p> <pre> 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</pre> <p>上面代码可解读为:</p> <ul> <li> <p>执行checked out HEAD~2</p> </li> <li> <p>在此之前,修改了提交信息</p> </li> <li> <p>在此之前,将特性分支合并进主分支</p> </li> <li> <p>在此之前,提交了快照</p> </li> </ul> <p>通过 <em>HEAD{<n>}</em> 语法你可以引用存在reflog中的提交。这与之前章节的 <em>HEAD~<n></em> 有着相似的用法,但<n>引用reflog中的记录而不是commit历史中的记录。</p> <p>你可以使用此方法回滚在别的记录中丢失的状态。例如,刚用 git reset 删除一个特性后,你的reflog会像下面这样:</p> <pre> 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</pre> <p>在 git reset 命令之前执行的三个操作现在处在悬空状态,这意味着若非使用reflog你将无法通过任何方法找到他们的引用。现在你知道你不应该丢掉你所有的工作了吧。你现在需要做的就是检出HEAD@{1}提交,将你的仓库退回到执行 git reset 之前的状态。</p> <pre> git checkout HEAD@{1}</pre> <p>这将把你的HEAD分离出来(和分支)从这步你可以创建一个新的分支继续你的特性开发工作。</p> <h2>小结</h2> <p>你现在应该很愉快地引用一个Git仓库中的commit。 我们学习了如何将分支和标签存储为.git子目录中的refs,如何读取packed-refs文件,如何表示HEAD,如何使用refspec进行高级 push 和 fetch ,以及如何使用相对 <em>〜</em> 和 <em>^</em> 字符在分支结构中切换。</p> <p>我们还了解了reflog,这是一种引用通过任何其他方式不可用的commit的方式。这一个你有种“起死回生”之感的操作。</p> <p>所有这一切的要点是能够精确地在开发方案中挑选出你的需要的commit。运用本文学到的知识对你已有的Git知识体系将有很大的提升:即对常用的命令 git log , git show , git checkout , git reset , git revert , git rebase 等命令使用 refs 作为参数。</p> <p> </p> <p>来自:https://segmentfault.com/a/1190000007996197</p> <p> </p>