我相信您可以在服务器上的 pre-receive 挂钩中检查这一点,但要定义允许的内容是很棘手的,并且它可能会使执行这些操作的人更加难以推送。而且,它只会给那些做错事的人一个错误:由他们来修复它,这对某些用户来说可能有点复杂。
此外,您(和开发人员)可能不想在某些合并上强制 --no-ff:特别是,如果您在分支 X 上没有提交并且您获取新的 origin/X 提交,您可能想要快速-将您的X 转发至origin/X。 (另一方面,在这种情况下,您/他们始终可以使用 git rebase。)
也就是说,让我们看看我们是否可以定义正确的行为。
首先,我们可能会注意到“快进”实际上是标签移动的属性,而不是合并的属性。当标签的先前提交是其新提交的祖先时,标签会以快进方式移动。 (因此,Git 使用术语“快进合并”来指代实际上根本不是合并的东西。)git 用于 any 分支推送更新的默认测试是标签更新 必须是快进操作,除非设置了强制标志。
我们不想拒绝标签快进,因为这是扩展分支的正常情况,无论是否合并:
...--o--o--n--n--n <-- br1
在此图中,我们表示一个提议的更新(如在 pre-receive 钩子中所见),现有提交写为o,新提交写为n。标签br1(更准确地说是refs/heads/br1)曾经指向最尖端的o,现在将指向最尖端的n。
在您的 pre-receive 钩子中,实际发生的情况是存储库实际上已经更新新的提交(合并与否),并且 git 只是将每个引用更新请求交给您,以<<em>old-hash, new-hash, name</em>> 元组的形式。换句话说,给定上面压缩的br1 更新图,我们可以用大写和小写(我不能做颜色,唉)用大写表示传入的old-hash和new-hash 值,给出:
...--o--O--n--n--N <-- br1
如果你拒绝推送,git 会结束垃圾收集新提交,因为你告诉它不允许任何引用更新(包括分支名称),所以 br1 最终仍然指向提交 O。
现在,合并在您的系统中通常是可以的,但您需要确保当br1 获得稍后将在br2 上的合并提交时,分支br2 不会 /em> 移动以直接包含该合并提交。也就是说,这样就OK了:
...--o--O--n--n--N <-- br1
... /
...---o--O--n--N <-- br2
甚至 this 都可以(也许——我们可能会假设您会在稍后的推送中收到 br2 更新,我们将检查 that 部分em>那么;我们还不能这样做,因为我们还没有得到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
(这里有人在br1 上做了git merge br2,然后在br2 上做了git merge br1 获得快进,然后在br2 上做了两次提交;他们还在br1 上做了两次提交; 然后他们再次将br1 合并为br2,然后将br2 合并为br1 作为--no-ff 合并;然后将br1 和br2 合并为一个git push)。
那么:确切地说,我们应该执行什么规则?我认为,我们可以通过在遍历合并提交时强制执行有关 --first-parent 的规则来使这更容易和。特别是,我们想要的是:
- 给定分支更新(不是创建或删除)
- 按图顺序 (
git rev-list --topo-order) 对 old-hash..new-hash 进行 --first-parent 遍历
- 要求结果列表中最早的提交将旧哈希作为其第一个父级。
有多种写法,尝试使用--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 merges",因为第一父 rev-list 不会返回原始哈希。
(我没有测试过这些。)