Git refs: An overview
通过这个以空间为主题的教程学习 Git 的基础知识。
通过了解引用提交的多种方式,可以使所有这些命令变得更加强大。在本章中,我们将通过探索许多引用提交的方法,阐明常用命令(如 git checkout
、git branch
和 git push
)的内部工作原理。
我们还将学习如何通过 Git 的引用日志机制访问看似“丢失”的提交,从而恢复这些提交。
哈希
引用提交的最直接的方法是通过其 SHA-1 哈希,这是每次提交的唯一 ID。您可以在 git log
输出中找到所有提交的哈希。
commit 0c708fdec272bc4446c6cabea4f0022c2b616eba Author: Mary Johnson Date: Wed Jul 9 16:37:42 2014 -0500 Some commit message
将提交传递给其他 Git 命令时,您只需要指定足够的字符来唯一标识提交。例如,您可以通过运行以下命令使用 git show
检查上述提交:
git show 0c708f
有时需要将分支、标记或其他间接引用解决为相应的提交哈希。为此,您可以使用 git rev-parse
命令。以下返回 main
分支所指向的提交哈希:
相关资料
如何移动完整的 Git 存储库
查看解决方案
了解 Bitbucket Cloud 的 Git
git rev-parse main
这在编写接受提交引用的自定义脚本时特别有用。您可以使用 git rev-parse
规范化输入,而不是手动解析提交引用。
引用
引用是指向提交的一种间接方式。您可以把它看作是提交哈希的用户友好别名。这是 Git 表示分支和标记的内部机制。
引用作为普通文本文件存储在 .git/refs
目录中,其中 .git
通常被称为 .git
。要浏览其中一个存储库中的引用,请导航到 .git/refs
。您应该会看到以下结构,但它会包含不同的文件,具体取决于您在代码存储库中有哪些分支、标记和远程文件:
.git/refs/ heads/ main some-feature remotes/ origin/ main tags/ v0.9
heads
目录定义了存储库中的所有本地分支。每个文件名都与相应分支的名称相匹配,在文件中您会发现一个提交哈希。这个提交哈希是分支尖端的位置。要验证这一点,请尝试从 Git 存储库的根目录运行以下两个命令:
# Output the contents of `refs/heads/main` file: cat .git/refs/heads/main # Inspect the commit at the tip of the `main` branch: git log -1 main
cat
命令返回的提交哈希应与 git log
显示的提交 ID 相匹配。
如需更改 main
分支的位置,Git 所要做的就是更改 refs/heads/main
文件的内容。同样,创建新分支只需将提交哈希写入新文件即可。这是 Git 分支与 SVN 相比如此轻巧的部分原因。
tags
目录的工作方式完全相同,但它包含的是标记而不是分支。remotes
目录将您使用 git remote
创建的所有远程存储库列为单独的子目录。在每个分支中,您会发现所有已提取到您的存储库中的远程分支。
Specifying 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
文件夹中的所有单个分支和标记文件移动到一个名为 packed-refs
的文件中,该文件位于 .git
目录的顶部。如果您打开这个文件,您会发现提交哈希到引用的映射:
00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature 0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/main bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9
在外部,普通的 Git 功能不会受到任何影响。但是,如果您想知道为什么您的 .git/refs
文件夹是空的,这就是引用的目的地。
Special refs
除了 refs
目录外,还有一些特殊的引用位于顶级 .git
目录中。它们如下所示:
HEAD
– 当前已签出的提交/分支。FETCH_HEAD
– 最近从远程代码存储库获取的分支。ORIG_HEAD
– 在HEAD
发生重大变化之前的备份引用。MERGE_HEAD
– 您正在使用git merge
合并到当前分支中的提交。CHERRY_PICK_HEAD
– 您正在挑选的提交。
这些引用都是由 Git 在必要时创建和更新的。例如,git pull
命令首先运行 git fetch
,它会更新 FETCH_HEAD
引用。然后,它运行 git merge FETCH_HEAD
以完成将提取的分支拉到存储库中。当然,您可以像其他任何引用一样使用所有这些,我相信您已经使用了 HEAD
。
这些文件包含不同的内容,具体取决于它们的类型和存储库的状态。HEAD
引用可以包含符号引用,它只是对另一个引用的引用,而不是提交哈希,也可以包含提交哈希。例如,当您在 main
分支上时,看看 HEAD
的内容:
git checkout main cat .git/HEAD
这将输出 ref: refs/heads/main
,这意味着 HEAD
指向 refs/heads/main
引用。Git 就是这样知道 main
分支当前已签出的。如果您要切换到另一个分支,HEAD
的内容将被更新以反映新的分支。但是,如果您要签出提交而不是分支,HEAD
将包含提交哈希而不是符号引用。这就是 Git 知道它处于游离的 HEAD 状态的原因。
大多数情况下,HEAD
是您将直接使用的唯一引用。其他脚本通常仅在编写需要钩子 Git 内部工作的较低级别脚本时才有用。
Refspecs
refspec 将本地存储库中的分支映射到远程存储库中的分支。这使得使用本地 Git 命令管理远程分支以及配置一些高级 git push
和 git fetch
行为成为可能。
refspec 指定为 [+]
<src>
:
<dst>
。<src>
参数是本地存储库中的源分支,<dst>
参数是远程存储库中的目标分支。可选的 +
符号用于强制远程存储库执行非快进更新。
Refspecs 可以与 git push
命令一起使用,为远程分支指定不同的名称。例如,以下命令将 main
分支推送到 origin
远程代码存储库,就像普通的 git push
一样,但它使用 qa-main
作为 origin
代码存储库中分支的名称。这对于需要将自己的分支推送到远程代码存储库的 QA 团队很有用。
git push origin main:refs/heads/qa-main
You can also use refspecs for deleting remote branches. This is a common situation for feature-branch workflows that push the feature branches to a remote repo (e.g., for backup purposes). The remote feature branches still reside in the remote repo after they are deleted from the local repo, so you get a build-up of dead feature branches as your project progresses. You can delete them by pushing a refspec that has an empty parameter, like so:
git push origin :some-feature
这非常方便,因为您无需登录远程存储库并手动删除远程分支。请注意,从 Git v1.7.0 开始,您可以使用 --delete
标记代替上述方法。以下命令将与上述命令具有相同的效果:
git push origin --delete some-feature
通过在 Git 配置文件中添加几行,您可以使用 refspecs 来改变 git fetch
的行为。默认情况下,git fetch
会提取远程存储库中的所有分支。其原因是 .git/config
文件的以下部分:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/*:refs/remotes/origin/*
fetch
行告诉 git fetch
从 origin
代码存储库下载所有分支。但是,有些工作流程并不需要全部。例如,许多持续集成工作流程只关注 main
分支。要仅提取 main
分支,请将 fetch
行更改为与以下内容匹配:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/main:refs/remotes/origin/main
您也可以用类似的方式配置 git push
。例如,如果您想始终将 main
分支推送到 origin
远程存储库中的 qa-main
(如上所述),您可以将配置文件更改为:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/main:refs/remotes/origin/main push = refs/heads/main:refs/heads/qa-main
Refspecs 使您可以完全控制各种 Git 命令如何在存储库之间传输分支。它们允许您重命名和删除本地存储库中的分支,提取/推送到不同名称的分支,并将 git push
和 git fetch
配置为仅处理您想要的分支。
Relative refs
您也可以引用相对于另一个提交的提交。~
字符允许您实现父项提交。例如,以下内容显示了 HEAD
的祖父项:
git show HEAD~2
但是,在处理合并提交时,事情会变得更加复杂。由于合并提交有多个父项,因此您可以遵循多条路径。对于三向合并,第一个父项来自您执行合并时所在的分支,第二个父项来自您传递给 git merge
命令的分支。
~
字符将始终跟在合并提交的第一个父项之后。如果您想关注另一个父项,则需要指定哪一个带有 ^
字符。例如,如果 HEAD
是合并提交,则以下内容返回 HEAD
的第二个父项。
git show HEAD^2
您可以使用多个 ^
字符移动多个世代。例如,这显示了 HEAD
(假设它是合并提交)的祖父项,位于第二个父项。
git show HEAD^2^1
为了阐明 ~
和 ^
的工作原理,下图显示了如何使用相对引用实现来自 A
的任何提交。在某些情况下,有多种方法可以实现提交。
相对引用可以通过与普通引用相同的命令一起使用。例如,以下所有命令都使用相对引用:
# Only list commits that are parent of the second parent of a merge commit git log HEAD^2 # Remove the last 3 commits from the current branch git reset HEAD~3 # Interactively rebase the last 3 commits on the current branch git rebase -i HEAD~3
The reflog
引用日志是 Git 的安全网。无论您是否提交快照,它都会记录您在存储库中所做的几乎所有更改。您可以把它看作是您在本地代码存储库中所做的一切按时间顺序排列的历史记录。要查看引用日志,请运行 git reflog
命令。它应该输出如下所示的内容:
400e4b7 HEAD@{0}: checkout: moving from main to HEAD~2 0e25143 HEAD@{1}: commit (amend): Integrate some awesome feature into `main` 00f5425 HEAD@{2}: commit (merge): Merge branch ';feature'; ad8621a HEAD@{3}: commit: Finish the feature
这可以翻译如下:
- 您刚刚看过了
HEAD~2
- 在此之前,您修改了提交消息
- 在此之前,您将
feature
分支合并到main
分支中 - 在此之前,您提交了快照
HEAD{
语法允许您引用存储在引用日志中的提交。它的工作原理很像上一节中的 HEAD~
引用,但是
引用的是引用日志中的条目,而不是提交历史记录。
您可以使用它来恢复到原本会丢失的状态。例如,假设您刚刚使用 git reset
取消了一项新功能。您的引用日志可能类似于下方的内容:
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
之前的三个提交现在悬而未决,这意味着除了通过引用日志外,没有办法引用它们。现在,假设您意识到您不应该丢掉所有工作。您要做的就是签出 HEAD@{1}
提交,以便在运行 git reset
之前恢复到存储库的状态。
git checkout HEAD@{1}
这会使您处于游离的 HEAD
状态。在这里,您可以创建一个新分支并继续开发您的功能。
摘要
现在,在 Git 存储库中引用提交应该很舒服。我们了解了如何将分支和标记作为引用存储在 .git
子目录中,如何读取 packed-refs
文件,如何表示 HEAD
,如何使用 refspecs 进行高级推送和获取,以及如何使用相对的 ~
和 ^
运算符遍历分支层次结构。
我们还看了引用日志,它是一种引用无法通过任何其他方式获得的提交的方法。这是从那些意外失误情况中恢复过来的好方法。
所有这一切的重点是能够在任何给定的开发场景中准确地挑选出您需要的提交。利用您在本文中学到的技能与您现有的 Git 知识相结合非常容易,因为一些最常见的命令接受引用作为参数,包括 git log
、git show
、git checkout
、git reset
、git revert
、git rebase
以及许多其他命令。
分享此文章
下一主题
推荐阅读
将这些资源加入书签,以了解 DevOps 团队的类型,或获取 Atlassian 关于 DevOps 的持续更新。