了解 git rev-list

7
在寻找Git Hook示例时,我遇到了以下帖子:https://github.com/Movidone/git-hooks/blob/master/pre-receive,我想了解以下命令:
git rev-list $new_list --not --all 

其中 new_list 是从以下内容中获得的:

NULL_SHA1="0000000000000000000000000000000000000000" # 40 0's
new_list=
any_deleted=false
while read oldsha newsha refname; do
    case $oldsha,$newsha in
        *,$NULL_SHA1) # it's a delete
            any_deleted=true;;
        $NULL_SHA1,*) # it's a create
            new_list="$new_list $newsha";;
        *,*) # it's an update
            new_list="$new_list $newsha";;
    esac
done

我认为rev-list按照时间倒序显示提交。

但是,有人能否分享一下-not-all选项的更多信息?

根据文档:

--not
Reverses the meaning of the ^ prefix (or lack thereof) for all following revision specifiers, up to the next --not.
--all
Pretend as if all the refs in refs/ are listed on the command line as <commit>. 

我无法完全理解这些选项。

[更新] 经过一些测试提交后,发现如果我不使用--not--all选项,那么git rev-list列出的是分支上所有提交记录,并非我打算推送的提交记录。

但是,我想知道为什么在传递--all选项时它不会在终端上打印sha值?


1
请注意,所提到的脚本是基于这个StackOverflow Q&A - torek
2个回答

20
git rev-list命令是Git中非常复杂、非常核心的命令,因为它所做的是遍历图形。这里的“图形”一词既指提交图形本身,也在某些情况下指从提交可达的Git对象的下一级。
我认为rev-list以相反的时间顺序显示提交。
不完全正确,但很接近:
顺序是可以更改的。默认值是相反的时间顺序。
默认值是遍历一些提交,但您可以让rev-list深入到包括树和blob对象甚至标签对象。这是针对像git fetchgit push(调用git pack-objects)和git pack-objects等程序的。我计划在这里完全忽略这种可能性,但我觉得至少应该提一下。
因此,默认值是以相反的时间顺序列出一些提交。指定git rev-list将要遍历的图形的确切部分——在“一些提交”中——是非常重要且有点棘手的。
但是,有人能分享一下--not--all选项的更多见解吗?
正如VonC所指出,这里的作用是列出接收存储库中新的提交。这取决于这个git rev-list命令在一个“pre-receive hook”中运行的事实。它通常在这个特定的钩子之外不会有任何有用的作用。因此,正如您所看到的,在Git中,钩子的运行时环境通常至少有一点特殊。 (这不仅适用于pre-receive钩子:必须考虑每个钩子的激活上下文。)
关于--not --all的更多信息 --all选项正是您从文档中引用的内容:
假装所有的refs/都列在命令行上…
所以这相当于执行git for-each-ref refs:它循环遍历每个引用。这包括分支名称(如mastermaindevelopfeature/tall等,所有这些都在refs/heads/中)、标签名称(如v1.2实际上是refs/tags/v1.2)、远程跟踪名称(如origin/develop实际上是refs/remotes/origin/develop)、替换引用(在refs/replace/中)、stash(refs/stash)、二分法引用、如果您正在使用Gerrit,则为Gerrit引用等。请注意,它不会循环遍历reflog条目。

--not前缀是一个简单的布尔运算。在gitrevisions语法中(请参阅the gitrevisions documentation),我们可以编写诸如develop之类的东西,意思是我告诉你从develop开始向后工作,并且包括这些提交,但也可以编写诸如^develop之类的东西,意思是我告诉你从develop开始向后工作,并且排除这些提交。因此,如果我写道:

git rev-list feature1 feature2 ^main

我要求Git遍历与名称为feature1feature2的提交相关的提交,但要排除与名称为main的提交相关的提交。关于可达性和图形遍历的一般想法,请参见像Git一样思考--not运算符有效地翻转了每个引用上的^
git rev-list --not feature1 feature2 ^main

作为简写,可以这样说:

git rev-list ^feature1 ^feature2 main

这会遍历从main可达的提交列表,但排除那些可达于feature1feature2的提交。

通常情况下,使用--all可以找到所有提交

如果你通常以正常方式使用Git,并且此时没有"分离头指针",则git rev-list选项中的--all告诉它包含所有提交,因为所有提交都可以从所有引用中到达。1因此,对任何本来会列出一些提交的git rev-list添加--not --all实际上是排除所有提交。输出为空:我们为什么要费心呢?

如果你处于分离头状态并且已经进行了多个新提交 - 例如,在交互式或有冲突的rebase过程中,就可能会发生这种情况 - 那么git rev-list HEAD --not --all将列出那些可从HEAD到达但不可从任何分支名称到达的提交。例如,在那个rebase中,这只是你已经复制的那些提交。

因此,“分离头”模式将是命令行中使用git rev-list --not --all的一个地方。但对于你正在检查的情况 - 一个pre-receive hook - 我们并不真正在命令行上。

Pre-receive hooks

当有人使用git push将提交发送到你自己的Git时,你的Git:

  • 设置隔离区来保存任何新对象(新提交和blob等);1
  • 与发送者协商决定发送者应该发送什么;
  • 接收这些对象;并且
  • 获取引用更新请求列表。这些更新请求基本上只是说使此名称保持此哈希ID2

在实际执行任何请求的更新之前,你的Git:

将整个列表提供给pre-receive hook。如果hook说“不行”,则整个推送将被拒绝。
如果hook返回“ok”,则逐个请求将列表提供给update hook。当hook返回“ok”时,执行更新。如果hook返回“no”,则您的Git会拒绝一个更新,但会继续检查其他更新。
在步骤2中接受或拒绝所有更新后,将接受的列表提供给post-receive hook。
需要的对象是添加到步骤2中某个ref中的,将从隔离区移动到Git的对象数据库中。被拒绝的对象则不会移动。
现在,考虑一下典型的git push。我们得到了一些新提交和一个请求:创建一个新的分支名称feature/short,或者我们得到了一些新提交和一个请求:更新现有的分支名称develop以包括这些新提交和旧提交。
在上述第1步中,我们有一个新的哈希ID。我们运行了一个循环来读取所有ref名称、它们的当前和拟议的新哈希ID,循环只运行了一次,因为只有一个名称正在进行git push。该哈希ID指的是新提交或提交,将被添加到此现有分支,或者是作为新分支的尖端和其他提交,这些提交是独占的。
现在,我们想要检查这些提交,而不是可从任何现有分支到达的任何现有提交。为简单起见,假设我们只有一个新的哈希ID $new和分支名称的旧哈希ID $old:如果分支是全新的,则为全零,否则为某个有效的现有提交。
如果新提交在完全新的分支上,则:
git rev-list $new ^master ^develop ^feature/short ^feature/tall

比如说,如果我们知道现有的分支只有这四个(并且没有需要担心的标签等),那么{{这些命令}}就可以涵盖它们。但是如果它们被添加到例如develop中,那么我们希望排除当前在develop上的提交。我们可以使用$old哈希值来实现:

git rev-list $new ^master ^$old ^feature/short ^feature/tall

这将再次列出只有那些运行git push origin develop的人想要添加到我们的develop中的新提交。

但是请考虑$old。这是一个哈希值。Git从哪里获取它?Git从名称develop获取了此哈希ID。这是一个pre-receive hook名称develop 尚未更新。因此,名称develop是旧哈希ID$old的名称。这意味着:

git rev-list $new ^master ^develop ^feature/short ^feature/tall

{{如果}}执行git rev-list $new {{并且}}跟随“而不是所有现有的”,那么:

同样完成任务。

git rev-list $new --not --branches

会完成工作。这几乎是我们这里所拥有的。

仅使用--branches存在一个问题,它无法获取任何标签或其他引用。我们可以使用--not --branches --tags,但--not --all更短,也可获取所有其他引用。

因此,这就是--not --all的来源:它依赖于pre-receive钩子的特殊情况。我们将由运行git push的人提出的新哈希ID列表列出为一系列行。我们使用git rev-list在隔离区中遍历建议更新的提交图,但不包括已在我们的存储库中的所有提交。rev-list命令生成这些哈希ID,每行一个,然后我们在shell循环中读取它们,并对每个提交进行检查


1隔离区是Git 2.11中新增的功能。在此之前,即使推送被拒绝,新对象也可能会在存储库中保留一段时间。对于大型服务器(如GitHub)来说,隔离区并不是什么大问题,但它可以为他们节省很多磁盘空间。

2请求可以强制执行或不强制执行,如果强制执行,则可以是带租约的强制执行,也可以不带。这些信息在pre-receive钩子(也不在update钩子)中不可用,这个情况,呃,让我们只说不太好,但添加它存在兼容性问题。尽管如此,这一切都是可以应对的。钩子可以告诉我们这是否是一个创建新引用删除现有引用的请求,因为如果是这样,旧哈希ID或新哈希ID中的一个将是所有零的“null哈希”(该哈希ID不允许为全零)。


5
这意味着:
列出通过从给定的提交(在此处为$new_list)沿父链接可达的提交,即新的、修改的或删除的提交, 但是排除通过在其前面加上^的给定提交(在此处为"all"),即所有HEADS提交或已标记的提交。
这将限制rev-list仅显示接收到的新提交,而不是所有提交(接收和已存在于接收存储库中的)。
请注意,现在可以将相同的限制应用于标准输入(stdin)与伪选项(pseudo-opts):
在Git 2.42 (2023年第三季度)中,get_revision() API的设置代码现在允许在--stdin模式下传递诸如--all--not等选项。
查看提交 c40f0b7, 提交 af37a20, 提交 cc80450 (2023年6月15日) 由Patrick Steinhardt (pks-t)完成。
(由Junio C Hamano -- gitster --合并于提交 812907d, 2023年7月4日) 修订版:在--stdin模式下处理伪选项 签名:Patrick Steinhardt 虽然git-rev-listgit-log都支持--stdin,但它只接受提交和文件。
值得注意的是,无法通过stdin传递任何伪选项,如--all--glob=或其他选项。

这使得在某些脚本场景中使用此功能变得困难,比如当我们想要支持针对特定修订版本和引用模式的查询时。
虽然理论上可以通过使用参数来实现,但一旦我们遇到足够大的查询时,可能会遇到平台限制的问题。
而且由于--stdin不能处理伪选项,唯一的替代方法就是使用参数和标准输入的混合方式,这很麻烦。

为了更好地支持这种用例,实现对这两个命令中伪选项的支持。
这里需要注意的一个限制是--stdin只支持以--glob=foo形式的"粘住"参数。
这是因为"不粘住"参数还需要我们读取下一行,这将给代码增加相当复杂性。
不过,对于脚本使用来说,这个限制应该是可以接受的。

rev-list-options现在在其man page中包括以下内容:

除了从命令行获取参数外,还可以从标准输入中读取参数。
这个选项接受提交和伪选项,如--all--glob=。 当看到一个--分隔符时,后面的输入将被视为路径并用于限制结果。

现在可以实现这一点:

git rev-list --stdin < --all --not --branches

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接