0

The problem of foxtrot merges explained in details in this question. In a nutshell: When working on the same branch (remote and local), the simple git merge command hides the remote merged commits under 2nd parent, and replaces the first-parents history of the remote (in the regular case - "origin"). And we consider it to be bad.

The linked-above question asks for a way to detect this situation, and reject the push. My question is how to automatically fix it.

I don't really care about the neat-tidiness of the full tree history, but I want that the first-parents tree will be perfect. And it is important to not disturb the other users, so the problem should solve itself automatically.

The goal: When the git origin hook detects foxtrot commit push, it will add another merge-commit, above the old-HEAD, and the new-HEAD, putting the old-HEAD as first-parent.

What is the safest way to achieve it?

2
  • Can't you simply disallow fast forwards on origin/master? Always require a merge commit? Commented Dec 21, 2021 at 14:50
  • @LasseV.Karlsen I didn't understand exactly how to do your suggestion, or how it'll help. (a) The new-HEAD is merge commit. (b) I don't want to block the users. I want that the users will push as they want, and that the server will fix it. Pull-Request processes (like in TFS) actually do kind-of what I want (they create new-merge-commit, with the correct first-parent). My problem with this is that we don't have PR (and it is an extra step for the user), and also - we don't work on TFS. Commented Dec 21, 2021 at 16:45

1 Answer 1

0

In the inspiration of the answers to the mentioned above question and to this question (merges in bare repo), I came out with the solution below.

This script should be put in the post-receive hook file. As requested, the script creates new-merge-commit that doesn't hide the old-HEAD.

The script also:

  1. Collects all the commit-msgs of the first-parent line of the new-HEAD, and sets it as the new-merge-commit msg.
  2. Fakes the author and committer of the new-merge-commit, to be as the author of the new-HEAD (while taking care cases of " or ' in the name).

Commands explanation:

  1. The git read-tree command stages the exact content of the new-HEAD commit. The new-merge-commit should be with the same exact content.
  2. The git log command collects the needed commit messages.
  3. The git ... commit-tree command creates the new-merge-commit with the author name/email, the staged data, the 2 parents, and the combined commit msg.
  4. The git update-ref command updates the relevant ref (in this case - master) to point to the new-merge-commit.

If there are problems with this script, it would be very appreciated if you'll spot them. :)

The script:

#!/bin/bash while read oldrev newrev refname do if [ "$refname" = "refs/heads/master" ]; then echo "Fix Foxtrot-Merges Hook..." MATCH=`git log --first-parent --pretty='%H %P' $oldrev..$newrev | grep $oldrev | awk '{ print \$2 }'` if [ "$oldrev" = "$MATCH" ]; then echo "...All is OK" else echo "...Fixing" authorName=`git log --first-parent --pretty='%an' $newrev^..$newrev` authorEmail=`git log --first-parent --pretty='%ae' $newrev^..$newrev` authorName=${authorName//"/\\"} authorEmail=${authorEmail//"/\\"} git read-tree -i --reset $newrev git log --first-parent --pretty='%B' $oldrev..$newrev > COMMIT_EDITMSG newCommitHash=$(git -c user.name="$authorName" -c user.email="$authorEmail" commit-tree $(git write-tree) -p $oldrev -p $newrev < COMMIT_EDITMSG) git update-ref $refname $newCommitHash fi fi done 
Sign up to request clarification or add additional context in comments.

11 Comments

This will mostly-work for the most common case, which might be good enough. However, the real problem here is that it can't be fixed because you cannot change existing commits. You can simply not accept it (which Git works well enough with), or you can pseduo-accept-it-and-rewrite-it as you are doing here. But when you do the pseudo-accept, the sending Git thinks you did a normal full accept and updates their remote-tracking name incorrectly. This may surprise others. It's better to get people to stop doing it in the first place, if you can.
Meanwhile, as a small refinement, consider using the existing merge commit's tree (git rev-parse ${rev}^{tree}) rather than reading a tree into the index and then writing out the index. Also, you can use git log -1 or git log --no-walk to extract author name and email and body text a bit easier. Unfortunately git log is technically porcelain rather than plumbing so it suffers from user-config-syndrome, but there's no plumbing equivalent to use.
Last: watch out for users who make several foxtrot merges, then push all of them with one git push. That's the case you're missing here: you only rewrite the tip commit.
@torek Thank you very much for your comments! Regarding to the first one, I see your points. In my case, it is good enough. In case of some crysis that should be managed very specificaly - we can temporarly disable this. About the second: Good ideas! I'll try it.
@torek About the last point, I think you are incorrect, or I didn't understand you. The old-HEAD (= the one that is the most recent origin/master that any other programmer and the build server knows about, until current user's push) will always be one first-parnet step from the auto-created commit (that is now the HEAD). No matter how many times the user git pull and it doesn't matter from which commits did the user pull; in the bottom line - the old-HEAD commit won't be hidden.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.