Git 就是与提交有关:您可以暂存提交、创建提交、查看旧提交,以及使用许多不同的 Git 命令在存储库之间传输提交。这些命令中的大多数都以某种形式对提交操作,其中许多都接受提交引用作为参数。例如,您可以使用 git checkout 通过传入提交哈希来查看旧提交,也可以使用它通过传入分支名称来切换分支。

引用提交的方法有很多

通过了解引用提交的多种方式,可以使所有这些命令变得更加强大。在本章中,我们将通过探索许多引用提交的方法,阐明常用命令(如 git checkoutgit branchgit push)的内部工作原理。

我们还将学习如何通过 Git 的引用日志机制访问看似“丢失”的提交,从而恢复这些提交。

哈希

引用提交的最直接的方法是通过其 SHA-1 哈希,这是每次提交的唯一 ID。您可以在 git log 输出中找到所有提交的哈希。

commit 0c708fdec272bc4446c6cabea4f0022c2b616eba Author: Mary Johnson  Date: Wed Jul 9 16:37:42 2014 -0500 一些提交消息

将提交传递给其他 Git 命令时,您只需要指定足够的字符来唯一标识提交。例如,您可以通过运行以下命令使用 git show 检查上述提交:

git show 0c708f

有时需要将分支、标记或其他间接引用解决为相应的提交哈希。为此,您可以使用 git rev-parse 命令。以下返回 main 分支所指向的提交哈希:

 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 存储库的根目录运行以下两个命令:

 # 输出 `refs/heads/main` 文件的内容: cat .git/refs/heads/main # 检查 `main` 分支顶端的提交: git log -1 main

cat 命令返回的提交哈希应与 git log 显示的提交 ID 相匹配。

如需更改 main 分支的位置,Git 所要做的就是更改 refs/heads/main 文件的内容。同样,创建新分支只需将提交哈希写入新文件即可。这是 Git 分支与 SVN 相比如此轻巧的部分原因。

tags 目录的工作方式完全相同,但它包含的是标记而不是分支。remotes 目录将您使用 git remote 创建的所有远程存储库列为单独的子目录。在每个分支中,您会发现所有已提取到您的存储库中的远程分支。

指定引用

将引用传递给 Git 命令时,您可以定义引用的全名,也可以使用一个简短的名称,让 Git 搜索匹配的引用。您应该已经熟悉了引用的简称,因为这是您每次按名称引用分支时所用的。

git show some-feature

上述命令中的 some-feature 参数实际上是分支的简称。Git 在使用之前将其解析为 refs/heads/some-feature。您也可以在命令行上指定完整的引用,如下所示:

git show refs/heads/some-feature

这样可以避免在引用的位置出现任何歧义。例如,如果您既有标记又有名为 some-feature 的分支,那么这是必要的。但是,如果您使用正确的命名惯例,标记和分支之间的歧义通常不应该成为问题。

我们将在 Refspecs 部分看到更多完整的引用名称。

打包引用

对于大型存储库,Git 会定期执行垃圾回收以删除不必要的对象,并将引用压缩为单个文件以提高性能。您可以使用垃圾回收命令强制进行压缩:

git gc

这会将 refs 文件夹中的所有单个分支和标记文件移动到一个名为 packed-refs 的文件中,该文件位于 .git 目录的顶部。如果您打开这个文件,您会发现提交哈希到引用的映射:

 00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature 0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/main bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9

在外部,普通的 Git 功能不会受到任何影响。但是,如果您想知道为什么您的 .git/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 pushgit 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

您也可以使用 refspecs 来删除远程分支。这是将功能分支推送到远程代码存储库(例如,用于备份目的)的功能分支工作流程的常见情况。远程功能分支从本地存储代码存储库中删除后仍驻留在远程代码存储库中,因此随着项目的进展,您会积累大量失效的功能分支。您可以通过推送一个带空 参数的 refspec 来删除它们,如下所示:

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 fetchorigin 代码存储库下载所有分支。但是,有些工作流程并不需要全部。例如,许多持续集成工作流程只关注 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 pushgit fetch 配置为仅处理您想要的分支。

相对引用

您也可以引用相对于另一个提交的提交。~ 字符允许您实现父项提交。例如,以下内容显示了 HEAD 的祖父项:

git show HEAD~2

但是,在处理合并提交时,事情会变得更加复杂。由于合并提交有多个父项,因此您可以遵循多条路径。对于三向合并,第一个父项来自您执行合并时所在的分支,第二个父项来自您传递给 git merge 命令的分支。

~ 字符将始终跟在合并提交的第一个父项之后。如果您想关注另一个父项,则需要指定哪一个带有 ^ 字符。例如,如果 HEAD 是合并提交,则以下内容返回 HEAD 的第二个父项。

git show HEAD^2

您可以使用多个 ^ 字符移动多个世代。例如,这显示了 HEAD(假设它是合并提交)的祖父项,位于第二个父项。

git show HEAD^2^1

为了阐明 ~^ 的工作原理,下图显示了如何使用相对引用实现来自 A 的任何提交。在某些情况下,有多种方法可以实现提交。

使用相对引用访问提交

相对引用可以通过与普通引用相同的命令一起使用。例如,以下所有命令都使用相对引用:

# 只列出合并提交第二个父项的父项提交 git log HEAD^2 # 从当前分支中移除最后 3 次提交 git reset Head~3 # 以交互方式对当前分支上的最后 3 次提交进行变基 git rebase-i HEAD~3

引用日志

引用日志是 Git 的安全网。无论您是否提交快照,它都会记录您在存储库中所做的几乎所有更改。您可以把它看作是您在本地代码存储库中所做的一切按时间顺序排列的历史记录。要查看引用日志,请运行 git reflog 命令。它应该输出如下所示的内容:

 400e4b7 HEAD@ {0}:签出:从 main 移动到 HEAD~2 0e25143 HEAD@ {1}:提交(修改):在 `main` 中集成一些很棒的功能 00f5425 HEAD@{2}:提交(合并):合并分支 ';feature'; ad8621a HEAD@{3}:提交:完成该功能

这可以翻译如下:

  • 您刚刚看过了 HEAD~2
  • 在此之前,您修改了提交消息
  • 在此之前,您将 feature 分支合并到 main 分支中
  • 在此之前,您提交了快照

HEAD{} 语法允许您引用存储在引用日志中的提交。它的工作原理很像上一节中的 HEAD~ 引用,但是 引用的是引用日志中的条目,而不是提交历史记录。

您可以使用它来恢复到原本会丢失的状态。例如,假设您刚刚使用 git reset 取消了一项新功能。您的引用日志可能类似于下方的内容:

ad8621a HEAD@ {0}:重置:移至 HEAD~3 298eb9f HEAD@ {1}:提交:其他一些提交消息 bbe9012 HEAD@ {2}:提交:继续该功能 9cb79fa HEAD@ {3}:提交:启动新功能

git reset 之前的三个提交现在悬而未决,这意味着除了通过引用日志外,没有办法引用它们。现在,假设您意识到您不应该丢掉所有工作。您要做的就是签出 HEAD@{1} 提交,以便在运行 git reset 之前恢复到存储库的状态。

git checkout HEAD@{1}

这会使您处于游离的 HEAD 状态。在这里,您可以创建一个新分支并继续开发您的功能。

摘要

现在,在 Git 存储库中引用提交应该很舒服。我们了解了如何将分支和标记作为引用存储在 .git 子目录中,如何读取 packed-refs 文件,如何表示 HEAD,如何使用 refspecs 进行高级推送和获取,以及如何使用相对的 ~^ 运算符遍历分支层次结构。

我们还看了引用日志,它是一种引用无法通过任何其他方式获得的提交的方法。这是从那些意外失误情况中恢复过来的好方法。

所有这一切的重点是能够在任何给定的开发场景中准确地挑选出您需要的提交。利用您在本文中学到的技能与您现有的 Git 知识相结合非常容易,因为一些最常见的命令接受引用作为参数,包括 git loggit showgit checkoutgit resetgit revertgit rebase 以及许多其他命令。