我相信您可以在服务器上的 pre-receive 钩子中检查这一点,但是要定义允许的内容很棘手,而且它可能会使那些执行它们的人更难以推送。此外,它只会给那些做错事的人一个错误:由他们来修复它,这对某些用户来说可能有点复杂。
此外,您(和开发人员)可能不想强制--no-ff
某些合并:特别是,如果您在分支上没有提交X
并且您获取新origin/X
提交,您可能希望快进X
到origin/X
. (另一方面,您/他们总是可以git rebase
在这种情况下使用。)
也就是说,让我们看看我们是否可以定义正确的行为。
首先,我们可能会注意到“快进”实际上是标签 move的属性,而不是合并的属性。当标签的先前提交是其新提交的祖先时,标签会以快进方式移动。(因此,Git 使用术语“快进合并”来指代实际上根本不是合并的东西。)git 用于任何分支推送更新的默认测试是标签更新必须是快进操作,除非设置了强制标志。
我们不想拒绝标签快进,因为这是扩展分支的正常情况,无论是否合并:
...--o--o--n--n--n <-- br1
在此图中,我们表示一个提议的更新(如在预接收挂钩中所见),现有提交写为o
,新提交写为n
。标签br1
(更准确地说,refs/heads/br1
)曾经指向最尖端o
,现在将指向最尖端n
。
在你的 pre-receive 钩子中,发生的事情是存储库实际上已经用新的提交(合并或不合并)进行了更新,并且 git 只是以元组的形式将每个引用更新请求交给你。换句话说,给定上面的压缩更新图,我们可以用大写和小写(我不能做颜色,唉)用大写表示传入和值来重写它,给出:<old-hash, new-hash, name>
br1
old-hash
new-hash
...--o--O--n--n--N <-- br1
如果你拒绝推送,git 会结束垃圾收集新提交,因为你告诉它不允许任何引用更新(包括分支名称),所以br1
最终仍然指向 commit O
。
现在,合并通常在您的系统中很好,但是您想要确保当br1
获得稍后将 on 的合并提交时br2
,分支br2
不会移动以直接包含该合并提交。也就是说,这是可以的:
...--o--O--n--n--N <-- br1
... /
...---o--O--n--N <-- br2
即使这样也可以(也许——我们可能会假设您会在以后的推送中获得更新,然后br2
我们会检查那部分;我们还不能这样做,因为我们还没有得到更新):br2
...--o--O--n--n--N <-- br1
... /
... n--n
... /
...----o--o <-- br2 (you get no update since br2 did not move)
但这将被拒绝:
...--o--O--n--n--N <-- br1, br2
... /
...---o--O--n--n
这是这样的:
...--o--O--n--n--N <-- br1
... / \
...---o--O--n--n N <-- br2
另一方面,这没关系(尽管我们可能想要限制允许它通过哪个父br2
级;大概在这些图中,直接通向左侧的线都是--first-parent
链接):
...--o--O--n--n--N <-- br1
... / \
...---o--O--n--n---N <-- br2
此外,在合并后获得额外的非合并提交是可以的:
...--o--O--n--n--n--N <-- br1
... / \
...---o--O--n--n---N <-- br2
(同样在 上br2
)。但是,我们必须检查每个合并,因为这是不行的:
...--o--O--n--n--n--n--n---N <-- br1
... / \ \ /
...---o--O--n--n n--n--N <-- br2
(这里有人git merge br2
在 onbr1
时做了,然后git merge br1
在br2
快进时做了,然后在 上做了两次提交br2
;他们也在 上做了两次提交br1
;然后他们再次合并br1
,br2
然后合并br2
为br1
一个--no-ff
合并;然后将两者都推br1
入br2
一个git push
)。
那么:我们应该执行什么规则呢?我认为,我们可以通过在遍历合并提交时强制执行规则来使这变得更容易和更好。--first-parent
特别是,我们想要的是:
- 给定分支更新(不是创建或删除)
- 遍历
--first-parent
,old-hash..new-hash
按图顺序 ( git rev-list --topo-order
)
- 要求结果列表中最早的提交具有作为其第一个父级的旧哈希。
有多种写法,尝试--boundary
使用--first-parent
. 所以让我们选择最简单的:
# Operation must be a fast-forward. This ensures that
# $oldsha is an ancestor of (and thus related to) $newsha,
# and thus we are not discarding any commits.
if ! git merge-base --is-ancestor $oldsha $newsha; then
... reject as non-fast-forward
fi
edge=$(git rev-list --topo-order --first-parent $oldsha..$newsha | tail -1)
# If the rev-list is empty then $oldsha is not related to $newsha.
# However, we checked that first. (The only other case where this
# can occur is if $oldsha equals $newsha, which is not an update,
# so we won't be running this code at all.)
#
# The listed edge commit may, however, be a root commit (have
# no parent). We must reject this case as well as "parent is
# not $oldsha". Fortunately that happens automatically since
# the empty string does not match a valid hash; we just need
# to be sure to quote "$parent".
parent=$(git rev-parse -q --verify $edge^)
if [ "$parent" = $oldsha ]; then
... update is OK
else
... reject as containing a bogus merge
fi
请注意,这也拒绝“foxtrot 合并”,因为第一个父项rev-list
不会返回原始哈希。
(我没有测试过这些。)