24

As part our rebase-heavy workflow, I am hoping to use a merge on the master branch. In particular I want to merge only when the topic branch has been rebased onto the most recent master commit thereby making any merge a fast-forward one. I can achieve that by using:

git merge --ff-only

Furthermore, I would like to record the integration of the topic branch with an empty commit which is why I would like to use --no-ff which forces that behaviour:

git merge --no-ff

What I really want, though, is a combination of both: merge only when it's trivial but let me record it anyway. Git thinks that

fatal: You cannot combine --no-ff with --ff-only.

which appears self-evident to some.

git merge --edit --no-ff topicbranch

doesn't achieve the behaviour I want either. So how can I merge with the --no-ff option iff the topic branch in question is rebased to the latest master commit?


UPDATE: Seems like Charles Bailey's answer will do the trick. If you desire to have this rolled into a git alias you could issue this here:

git config --global alias.integrate '!test "$(git merge-base HEAD "$1")" = "$(git rev-parse HEAD)" && git merge --no-ff --edit $1 || echo >&2 "Not up-to-date; refusing to merge, rebase first!"'

A bit of a mouthful but works. Note the force of a commit message edit with option --edit.

DrSAR
  • 1,522
  • 1
  • 15
  • 36
  • The --ff-only is being issued in the title. I'll amend the text to make it clearer. And I am using the --ff-only as a check to see whether the merge is a trivial one. @CharlesBailey, I find you're quite liberal with your down votes. tsk tsk. – DrSAR Mar 26 '13 at 08:06
  • 3
    So basically you don't want `--ff-only` because you *do* want to make a merge commit. You can perform the check as a separate step: e.g. `test $(git merge-base HEAD topicbranch) = $(git rev-parse HEAD) && git merge --no-ff topicbranch` – CB Bailey Mar 26 '13 at 08:10
  • Could you roll this into an alias and put it in an answer and I'll vote you up. – DrSAR Mar 26 '13 at 08:11
  • 1
    Just for reference some discussion available [here](http://git.661346.n2.nabble.com/PATCH-merge-allow-using-no-ff-and-ff-only-at-the-same-time-td7590816.html): "If one were designing Git merge from scratch today, however, I could see one may have designed these as two orthogonal switches." – Jaakko Luttinen May 10 '16 at 10:16
  • 1
    Nowadays it's pretty common to desire to do this when completing a Pull Request. I've outlined which of the most popular tools offer this feature, which is called Semi-Linear Merge in Azure Devops. Details can be found in the Side Note of [this answer](https://stackoverflow.com/a/63621528/184546). – TTT Mar 26 '22 at 05:42

3 Answers3

13

You don't want --ff-only because you do want to make a merge commit which --ff-only would inhibit.

The check that you want can be made as a separate check before running the merge command. You could package this into a simple shell function.

E.g.

merge_if_ahead () {
  if test "$(git merge-base HEAD "$1")" = "$(git rev-parse HEAD)"; then
      git merge --no-ff "$1"
  else
      echo >&2 "Not up to date; refusing to merge"
      return 1
  fi
}
CB Bailey
  • 755,051
  • 104
  • 632
  • 656
  • 1
    Hi, do you have any idea if I can use this to enforce it say on Bitbucket or Github? Is it possible to achieve that using Git Hooks? – Raphael Oliveira Nov 30 '16 at 04:47
2

I've created an alias that works for my simple tests

git config --global alias.ffmerge '!sh -c "git merge --ff-only $1 && git reset --hard HEAD@{1} && git merge --no-ff $1" -'

It consists of 3 commands, one after each other, that will fail as soon as the first one fails. Explaining each:

git merge --ff-only $1

Will fail if the merge cannot continue as a fast-forward merge. ($1 is the branch name passed in as argument)

git reset --hard HEAD@{1}

If the first command succeeds it means we can do a fast-forward merge but as we want to do a merge commit we'll move back to the state of HEAD just before our (successful) merge. HEAD@{1} is a reflog pointer.

git merge --no-ff $1

Now we'll do the real merge with a merge commit even if the merge can go a head as a fast-forward.

A simple call to git ffmerge will do the entire thing for us, the only step that can fail is the first and that would leave us in an unchanged working copy as before.

As always with git reset --hard the working copy should be clean.

Andreas Wederbrand
  • 38,065
  • 11
  • 68
  • 78
  • Reset hard will throw away all unstaged changes. This is unnecessary and dangerous in the context of what actually needs to be checked. (i.e. You should only need to check whether a fast-forwards is possible but now you _should_, but don't check whether staged or unstaged changes are present.) – CB Bailey Mar 26 '13 at 11:11
  • @Andreas Wederbrand Thanks for this. Despite the dicey hard reset, it works for my purposes (and is less dependent on shell issues like the -z solution). You get a +1 to compensate for other people's ferocious voting habits. Even though I will try to get bereal's solution cast into a working alias. – DrSAR Mar 26 '13 at 15:52
  • I agree that reset --hard might be dangerous and that is why I mentioned that the working copy should be clean. This kind of merges would typically happend on some kind of merge machine where nothing else happens. – Andreas Wederbrand Mar 26 '13 at 15:59
2
git config alias.mff '!mff() { git merge --ff-only "$1" && git reset --hard HEAD@{1} && git merge --no-ff "$1"; }; mff'

and from then on

git mff other-branch
Vampire
  • 35,631
  • 4
  • 76
  • 102
  • How is this different from @andreas-wederbrand 's answer? – DrSAR Mar 11 '17 at 18:45
  • 1
    Oh, actually I overlooked his solution. But as you ask, my solution does not start a new `sh` executable, but simply defines a function that is called. This saves the cost of starting another process. – Vampire Mar 12 '17 at 17:11