4

I would want to add a hook on my Gitlab server to prevent pushing merged branches on master if they was not rebased before.

For example : A---B---C---D ← master \ E---F---G ← new-feature

I want the user to rebase his feature before merging/pushing. A---B---C---D-------------H ← master \ / E'---F'---G'

I don't want this to be pushed A---B---C---D---H ← master \ / E---F---G

This, is a good beginning but I don't find mean to only refuse not empty merge commits : Force Feature Branch to be Rebased Before it is Merged or Pushed

Benito103e
  • 360
  • 3
  • 12

4 Answers4

6

If you are still looking for this. gitlab is the only git server that implements this. they called it semi-linear history. look at the 2nd option gitlab configuration

This will ENFORCE this kind of history (look at the right) seemlessly inside your merge request: enter image description here

Yosef-at-Panaya
  • 676
  • 4
  • 13
3

It is definitely possible, but you need to write some code. You must also decide what precisely defines a "good" commit-graph update. Your example says that a request to go from this:

o--o--o--*   <-- master

to this:

o--o--o--*---o   <-- master
    \       /
     o--o--o

is to be rejected, while this:

o--o--o--*---------o   <-- master
          \       /
           o--o--o

is to be accepted. But what about this third alternative:

o--o--o--*------o-----o   <-- master
          \    /     /
           o--o--o--o

This adds two merges rather than just one; but no new commit's merge has any parent that is an ancestor of the prior value of master.

And, what about this?

o--o--o   <-- master

(Here the push has removed the commit that used to be the tip of master.)

If the second push, that adds two merges but none of them reach back to any earlier commits, is not to be accepted, and the last push is also not to be accepted, part of your task is pretty easy: you want to allow at most one merge, perhaps restricting it to exactly two parents with one of its two parents—perhaps this must even be "the first parent"—being the prior value of master (the commit marked *). The rest of your task is probably to allow no merges at all, as long as the proposed new master is not an ancestor of the old master (no commits are to be removed).

If the second (two-merge) push is to be accepted, the coding will be trickier. Note that if it's not to be accepted, someone can still push such a merge, they just have to do it in multiple steps (one push per merge).

torek
  • 448,244
  • 59
  • 642
  • 775
1

The usual reason to force rebasing is a pathological hatred of merge commits because the person setting the policy doesn't see their value. It's not clear why you'd want to take the disadvantages of rebase (odds are the intermediate commits will not have been tested) but still have the merge; this seems to me like the least valuable merge commit possible. But if you must...

You imply that the merge you want to accept would be "empty"; that's not exactly true. It applies changes to its first parent (though not to its 2nd parent, since it would be a fast-forward if allowed to be).

What I think you're really saying is that you would accept a merge if the first parent is reachable (via parent pointers) from the second parent. So you could take the output of

git rev-list --merges $oldrev..$newrev

and feed each resulting commit ID as the commit-ID arguments in

git merge-base --is-ancestor commit-ID^ commit-ID^2

rejecting if the merge-base command ever returns non-zero.

(Technically I guess you might also want to make sure the commit didn't have 3 or more parents.)

That still allows something like this

     (origin/master)
            |
x -- x -- x -------------------- M <--(master)
           \                    /
            x -- x -------x -- x
                  \      /
                   x -- x

If avoiding that is a rule, it's significantly harder; you basically would want every merge to be reachable via first-parent pointers from the head commit. (But you can't just say that merges should be reachable from the head commit's first parent, because then you'd still allow

     (origin/master)
            |
x -- x -- x -------------------- M -- x<--(master)
           \                    /
            x -- x -------x -- x
                  \      /
                   x -- x

which is the same thing.) So you could maybe, as a "first step" before you start looking for merges, do a

git rev-list --first-parent $oldrev..$newrev

and hang onto a list of all the commit ID values that returns, so as you find each merge you can confirm that it's in that list.

If this all sounds like no fun at all, I couldn't agree more; which is why I'm not going to the trouble of trying to assemble a working script from this advice, and why I recommend you either allow merges or don't instead of trying to take such an unusual middle ground.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
  • 1
    Thank you for your detailed answer. A rebase feature branch would have been tested with last changes of the master. I accept fast-forward merge but sometimes it is interesting to keep the drawing of the branch. – Benito103e Jun 16 '17 at 15:08
  • 2
    The *last commit of* a rebase feature branch would have been tested. The intermediate commits, not so likely. Unless you test and fix each *prior* commit from the now-rebased branch, you likely end up with a history of `x--x--O--x--O--x--x--x--O` where the `x` may not build or function properly, which is particularly troublesome if you ever want to use `bisect` to track down a defect. – Mark Adelsberger Jun 16 '17 at 15:47
  • It's a good demonstration, in fact yes, you need to test, eventually fix all prior commits after a rebase. – Benito103e Jun 20 '17 at 11:57
  • 1
    @mark: when you force to merge with no-ff you keep your integration branch clean of any "checkpoint commit" as you can follow the deliveries using the first parent log and from there you can bisect on first parents only (see https://stackoverflow.com/questions/5638211/how-do-you-get-git-bisect-to-ignore-merged-branches ) and find the guilty merge – Yosef-at-Panaya Jan 13 '20 at 21:01
  • And having found the guilty merge, you're nowhere near as close to finding the bug as if you had been able to bisect right to the guilty commit. – Mark Adelsberger Jan 14 '20 at 07:58
  • I don’t see why you bring up “That still allows something like this” as some follow-up problem that necessarily follows. Sure, maybe in the “don’t like merges” ideology that rule would come up, but that’s still a separate rule from what the OP is asking for. All the question is asking for is (as you formulated it) “accept a merge if the first parent is reachable (via parent pointers) from the second parent”. – Guildenstern Mar 07 '23 at 14:17
1

pre-receive hook:

#!/usr/bin/env python3

import fileinput
import sys
import subprocess

protected_branch = 'main'

for line in fileinput.input():
    s = line.split()
    old = s[0]
    new = s[1]
    ref = s[2]
    if ref == 'refs/heads/' + protected_branch:
        first_parent = subprocess.check_output(["git", "rev-parse", new + "^1"]).strip().decode()
        if first_parent != old:
            error = 'Rejected: Not a semi-linear update. See: https://stackoverflow.com/q/44500174/1725151'
            print(error, file=sys.stderr)
            exit(1)

This will also deny updates that are not merges if they consist of more than one commit. You might want to change that if you are fine with fast-forward merges.

This can be expanded to also incorporate the deny-merges check in the other answer.

Guildenstern
  • 2,179
  • 1
  • 17
  • 39