Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Foxtrot Merge: How to solve it

I have been working on a project and have been pushing code to my dev branch on remote git repository. Meanwhile, some changes were introduced in the master branch that I needed to fetch. So i used the following route: 1 - git checkout master 2 - git pull 3 - git checkout my_dev_branch 4 - git merge master

Now when I committed my changes to a pull request into master, It shows that the merge i performed is foxtrot merge. Should I revert or reset this earlier merge commit and then manually fetch those changes from master. Or is there any other solution available to this.

Currently, I have been looking to just revert to a commit before the merge but this article here "https://mirrors.edge.kernel.org/pub/software/scm/git/docs/howto/revert-a-faulty-merge.txt" presents quite a gloomy and complicated picture. I don't want it to be that complicated of a task. This answer (How to revert a merge commit that's already pushed to remote branch?) is good enough? Thank you all.

like image 632
Ali. K Avatar asked Dec 22 '22 22:12

Ali. K


1 Answers

First, the commands you listed don't by themselves lead to a "foxtrot merge". (See also GIT: How can I prevent foxtrot merges in my 'master' branch?) If you have one of these, you must have run git merge from your master to your origin/master, probably via git pull, after making a commit on your own master. (This make-a-commit-on-your-own-master part is what doesn't show in your sequence of commands.)

In any case, you really don't want to revert the merge. That leaves the merge in place, and adds another commit that has the effect of undoing the merge in terms of its effect on the source. It's this second non-merge commit that leads to the subsequent mess in your linked kernel.org page. If you do do this revert, what you end up with is a foxtrot merge, followed by more commits including the revert, followed by whatever you have to do later to re-merge the merge anyway. So the foxtrot merge is still there!

In general, foxtrot merges are not all that bad. They just have the main line work as the second parent of a feature, instead of as the first parent of a feature—but who is to say which line is the main line in the first place? If you change your perspective, perhaps your feature was the main line after all, and the thing that everyone else has been calling the "main line" was actually the side feature. That's all a foxtrot merge is: a change in perspective, to claim that you are the main line, and they are the side feature.

But if you don't like that, the only true cure is to rewrite your own history. Since it's your history—your merge that declares that your commits are your main line—this might be OK, not only with you but with everyone else too. It is OK if everyone else who has obtained your commits—your history—is not already depending on, and building on, these commits. If they are depending on, and building on, these commits, it may still be OK: are all these other colleagues or coworkers OK with redoing their work, after you rewrite your own history?

If all of that is true—if no one else depends on your history, or all of those who do depend on it are willing to do even more work so that they can handle your rewriting of your history—then what you are to do is clear: remove your foxtrot merge from your master. This requires careful use of git reset, and understanding how Git branches really work, what git merge does, and how branch names are not the key. The key is the commit graph.

Making new commits adds to the graph, and updates whichever branch name you're "on"—in the sense that git status says "on branch ___"—so that that branch name identifies the new commit. The new commit's parent—singular, in the case of most commits—is the commit that was the tip of the branch before. If the new commit is a merge commit, the new commit's parents, plural, are the commit that was the tip of the branch before (as usual), and then—as a second parent—the commit you specified when you said to merge.

That is, a branch in Git is just a chain of commits:

... <-F <-G <-H   <--master

The name master remembers a hash ID, in this case, H (H stands in for the actual hash ID, which is a big ugly string of hexadecimal representation of a 160-bit number, and which appears random, though it isn't). Commit H is the last commit on the branch. Git uses the hash ID in master to find the actual commit. Commit H itself contains, i.e., remembers, a parent hash ID, G, that Git uses to find commit G. Commit G remembers its parent F; Git uses this hash ID to find the actual commit F. This process repeats, with Git walking the chain backwards, one commit at a time, to find all the commits that are "on"—more precisely, reachable from—the branch name.

When a branch name, or a commit, contains a hash ID for some other commit, we say that the branch name (or commit) points to the commit whose hash ID they contain. So a branch name points to a—one, single—commit. That commit is the tip of the branch. That's simply Git's definition of "branch".

If you make some change(s) to some file(s) and then git commit the result, the new snapshot gets a new and unique big ugly hash ID, which we can call I. The new commit I records hash ID H, so that I points back to H. Then Git writes I's commit hash ID into the name master, so that master points to I:

... <-F <-G <-H <-I   <--master

and that's how a branch grows.

When you use git reset (in the right way), you tell Git: Change master so that instead of pointing to my new commit I, it points back to H instead. Commit I is not actually gone: it's still there, floating in space as it were. But if master was the only name you had for it, it's now very hard to find its hash ID. (Do you have it memorized? :-) ) Now instead of master pointing to I and I pointing to H, now you have:

             I
            /
...--F--G--H   <-- master

This is also how you get rid of a foxtrot merge. Suppose you started with:

...--F--G--H   <-- master

Someone else added their new I and J commits to their master (which your git fetch remembers as your origin/master):

...--F--G--H   <-- master
            \
             I--J   <-- origin/master

You add a new commit, perhaps even a whole string of them, to your master, perhaps by merging your own feature/tall:

             K--L   <-- feature/tall
            /    \
...--F--G--H------M   <-- master
            \
             I--J   <-- origin/master

Here, your merge commit M has shared H as its first parent (the "main line") and your commit L as its second parent (your feature). Then you add a new commit N that makes their J, their master that's your origin/master, into a side branch, because your M is your main line:

             K--L   <-- feature/tall
            /    \
...--F--G--H------M--N   <-- master
            \       /
             I-----J   <-- origin/master

Their J is now a side branch, just like your feature/tall, because the main line runs N, M, H, G, F, ..., straight back along the main line—the sequence of first-parents.

If you want to perceive the main line as going back through J, you must eliminate your merge M entirely. This requires making your master point to H again. You might do this with:

git checkout master
git reset --hard <hash-of-H>

which we can then draw (messily) as:

               L   <-- feature/tall
              / \
             K ,-M-----N
            /_/       /
...--F--G--H   <-------- master
            \       /
             I-----J   <-- origin/master

Note that all the commits are still there—N still goes back to M and J as before, and M still goes back to both H and L as before—but there is no name that leads to N, and hence no way to find N or M. We can therefore drop them from the drawing:

             K--L   <-- feature/tall
            /
...--F--G--H   <-- master
            \
             I--J   <-- origin/master

Now all you need to do is slide your master forward-and-down to J, which you can do with git merge --ff-only origin/master:

             K--L   <-- feature/tall
            /
...--F--G--H
            \
             I--J   <-- master, origin/master

which we can now redraw even-less-messily:

             K--L   <-- feature/tall
            /
...--F--G--H--I--J   <-- master, origin/master

You're now ready to make a pull request, i.e., a request that someone else run git merge for you, in which you propose making a new merge—we can call it M again if we like—that will have as its first parent, commit J, and as its second parent, commit L: a non-foxtrot merge that calls the main line "the main line" and your feature a "feature".

This stuff is kind of hard. The trick to understanding it is that commit hash IDs are universal: they're globally unique, and every repository that gets your commits, gets them by their hash IDs. Your Git, and Joe's Git, Katherine's Git, Larry's Git, and so on, all agree that commit H is commit H. It never has any other hash ID. But the names that your Git uses—your branch names—are yours. They don't need to appear in any other repository. The names they use—their master, their develop, and so on—will show up in your Git as your origin/master, origin/develop, and so on. Running:

git fetch

makes your Git call up their Git, get any new commits—by hash IDs as always—and then update your origin/* names based on their names. It's the fetch that gets their commits and updates your remote-tracking names, your origin/*.

When you run git merge --no-ff, or have Bitbucket or GitHub do this for you, that creates a new commit—which automatically gets a new, unique hash ID—and it is the new merge commit that records a first parent—the "main line"—and a second parent, the "feature, all by their hash IDs. Any names, whether they're your branch names like master or your remote-tracking names like origin/master, become irrelevant once you actually have and keep the commit with its unique hash ID.

To put it another way: Names find commits. Only commits really matter. Names matter only if, when, and because they find commits.

(See also how to avoid foxtrot merge in git. especially the comments.)

like image 139
torek Avatar answered Jan 05 '23 05:01

torek