git merge: enforce both linear history AND merge commits
Asked Answered
L

1

5

In our project, we want to have merged feature branches visible in git log --graph, but still have a linear history, so that there are no commits on the main branch for the duration of the feature branch. That means, ensure that a feature branch is rebased on main before it's merged into it.

At the moment, we have aliased merge = merge --no-ff and try to remember always doing git rebase before merging, but sometimes we forget.

I.e. if the situation before the merge looks like this:

A--B--C--D
    \
     E--F

we want to enforce this outcome (0)

A--B--C--D------m--
          \    /
           E--F

and discourage this (1)

A--B--C--D--m--
    \      /
     E----F

or this (2)

A--B--C--D--E--F--

Unfortunately, I can not see how to achieve this with git aliases.

  • git merge --no-ff creates merge commit, but allows outcome (1)
  • git merge --ff-only ensures there's no commit on main branch, but creates outcome (2)
  • git merge --ff happily creates (1) or (2), but no (0)
  • git merge --ff-only --no-ff behaves like --no-ff

We all need to commit to the main branch independently of others, so solutions based on limiting access to the main branch or code review are not really helpful.

Logging answered 21/10, 2022 at 16:21 Comment(4)
I am pretty certain that there is no builtin feature in git merge that enforces a fast-forward situation, but then makes a commit anyway. For the Git developers, such a workflow is just not sensible enough to be supported out of the box.Ium
Are you all merging your branches into main locally and then pushing main?Yongyoni
@Yongyoni yes. These are all local (file:///) repositoriesLogging
@Logging ok, in that case I think the pre-merge hook in my answer below is probably your best bet. You'll need to ask everyone to install the hook, so you can't force them to use it (just like you can't force the use of aliases), but it sounds like that shouldn't be a problem.Yongyoni
Y
6

Nomenclature alert: I wouldn't recommend calling this a "linear history" because without context that almost always means not having merge commits at all.

What you're trying to achieve is typically called a "semi-linear merge" or "rebase, merge" strategy. This provides feature "bubbles", and those bubbles appear linearly in the log. Note that some people allow skipping the merge commit when the feature branch has exactly one commit, because functionally there is no information loss in the resulting graph. You should decide going in whether you will allow this. (My personal preference is to not allow it for consistency sake. But that means we end up with a lot of tiny one commit + one merge commit bubbles, which doesn't bother me.)

As for how to achieve this, I don't recommend the alias idea as you presented. There are just too many scenarios where you might want to fast-forward merge in your day to day work in your local repo.

Instead, I would first lean towards enforcing the policy on the server side, and if that's not possible then you could try using hooks. For server side implementation, here's an answer which discusses popular tools and their capabilities regarding this strategy. (Currently both Azure DevOps and Bitbucket can enforce this out of the box.) If you're using a tool that doesn't have built in support, consider implementing some sort of gated checkin that validates the source branch is fully up to date with the target before allowing the merge to complete (or the push). There are many ways to do that check, one of which might be (using Bash):

# return the target branch tip commit if it passes, nothing otherwise:
git rev-list <source-branch> | grep $(git rev-parse <target-branch>)

Note regarding this statement:

We all need to commit to the main branch independently of others, so solutions based on limiting access to the main branch or code review are not really helpful.

You can still enforce a Pull Request completion strategy without requiring other people to approve your code. For example in Azure DevOps, you would require a PR to merge a branch into main, and then you would only allow semi-linear merge. AzDO will rebase it for you automatically (if needed), and then create the merge commit, and you still don't need to involve another person. I'm pretty sure Bitbucket has this same capability out of the box. With GL or GH you would require the MR/PR, but allow self completion, and turn on the gated checkin to only allow you to complete it if it's fully up to date.

If you're merging into main locally and pushing directly the remote main, the other mechanism would be using Git hooks. Perhaps the pre-merge or the pre-push hooks would work best. Inside of the hook, you could look at the HEAD commit of main, and make sure that the following are true:

  1. It is a merge commit.
  2. That the merge-base of the two parent commits (@^1 and @^2) equals the same commit as the first parent (@^1).

Side Note: It's fine to enforce this strategy when merging feature branches into a shared branch. However, if you use a workflow that has more than one shared branch, you don't want to rebase a shared branch onto another shared branch, so make sure you use just plain old merge when you're doing those merges.

Yongyoni answered 21/10, 2022 at 17:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.