Foxtrot Golf India Tango

Foxtrot Golf India Tango

January 28, 2024

What is a foxtrot merge, how to fix it and who is my parent #1.

Why

Inspired by 2016 blog post No Foxtrot Merges Allowed In My Git, repostet on Atlassian Developer Protect our Git Repos, Stop Foxtrots Now! by Sylvie Davies. I started to play around a bit and check hands-on what happens when I merge and rebase. Read the original post and do the foxtrot with me.

Do the foxtrot

Read the post. TL;DR always keep the main branch as first parent in merges.

I created an example repo on codeberg foxtrot, you can clone it and do the foxtrot with me.

The initial state is:

git log main feat --oneline --graph

* 24d3250 (origin/main, origin/HEAD, main) 6
| * 707f11d (HEAD -> feat, origin/feat) f1
|/  
* c350b81 5
* 6f9114d 4
* 3f9e923 3
* c27fadf 2
* 9057d20 1
* ccf0b92 0

There is a feature branch, feat, that has commit 5 as common ancestor with main.

Very often developers merge main into feat to stay up to date with code changes.

git switch first-parent, here feat git merge seconde-parent, here main

git switch feat
git merge main
git log main feat --oneline --graph

Here comes the foxtrot, commit f1 moved magically in the first parent lane, the one on the left, and commit 6, from the main branch, is in the second parent lane.

*   xxxxxxx (HEAD -> feat) Merge branch 'main' into feat
|\  
| * 24d3250 (origin/main, origin/HEAD, main) 6
* | 707f11d (origin/feat) f1
|/  
* c350b81 5
* 6f9114d 4
* 3f9e923 3
* c27fadf 2
* 9057d20 1
* ccf0b92 0

git log -1 <merge-hash> shows you the parents of the merge commit.

commit xxxxxxxxxxxxxxxxxxxxxxxx (HEAD -> feat)
Merge: 707f11d 24d3250
Author: otherpaco <otherpaco@noreply.codeberg.org>
Date:   Sun Jan 28 17:15:50 2024 +0100

    Merge branch 'main' into feat

707f11d first parent
24d3250 second parent

Merge the foxtrot into main

When you are done with your feature and merge feat into main, this will be done with a fast-forward merge and results in a foxtrot:

git switch main
git merge feat
git log main feat --oneline --graph
*   xxxxxxx (HEAD -> main, feat) Merge branch 'main' into feat
|\  
| * 24d3250 (origin/main, origin/HEAD) 6
* | 707f11d (origin/feat) f1
|/  
* c350b81 5
* 6f9114d 4
* 3f9e923 3
* c27fadf 2
* 9057d20 1
* ccf0b92 0

Right back where we started from - RESET

Whenever you play around a bit, this commands will bring you back to the initial state.

git switch feat
git reset --hard 707f11d
git switch main
git reset --hard 24d3250
git log main feat --oneline --graph

We should be in the initial state

* 24d3250 (HEAD -> main, origin/main, origin/HEAD) 6
| * 707f11d (origin/feat, feat) f1
|/  
* c350b81 5
* 6f9114d 4
* 3f9e923 3
* c27fadf 2
* 9057d20 1
* ccf0b92 0

Rebase to the rescue

When you are at the end of the ‘do the foxtrot’ part,even if you merged main mulitple times into feat, the rebase command will straigthen out your history, and rewrite your histroy - see this great article git rebase: what can go wrong? by Julia Evans. If you want to push your feat-branch you now have to use the power of the --force (git push --force-with-lease).

git switch feat
git rebase main
git log main feat --oneline --graph
* xxxxxxx (HEAD -> feat) f1
* 24d3250 (origin/main, origin/HEAD, main) 6
* c350b81 5
* 6f9114d 4
* 3f9e923 3
* c27fadf 2
* 9057d20 1
* ccf0b92 0

Everything looks just fine, let’s see what happens now:

git switch main
git merge feat
git log main feat --oneline --graph

This was a fast forward merge, too.

* xxxxxxx (HEAD -> main, feat) f1
* 24d3250 (origin/main, origin/HEAD) 6
* c350b81 5
* 6f9114d 4
* 3f9e923 3
* c27fadf 2
* 9057d20 1
* ccf0b92 0

Everything is fine.

Always rebase in feat-branches

Instead of merging main, one could always use git rebase main in feat-branches, if you don’t share the branch with others.

No fast-forward

As Sylvie Davies suggests, --no-ff is another solution but an ugly one imho.

Back to the ‘do the foxtrot’ part.

The next command is very close to the one in merge the foxtrot into main but we add --no-ff to prevent a fast-forward merge.

git switch main
git merge feat --no-ff
git log main feat --oneline --graph
*   xxxxxxx (HEAD -> main) Merge branch 'feat'
|\  
| *   xxxxxxx (feat) Merge branch 'main' into feat
| |\  
| |/  
|/|   
* | 24d3250 (origin/main, origin/HEAD) 6
| * 707f11d (origin/feat) f1
|/  
* c350b81 5
* 6f9114d 4
* 3f9e923 3
* c27fadf 2
* 9057d20 1
* ccf0b92 0

Commit 6 is back in the first parent lane but overall the history looks a bit wild.

No updates for me

Back to the ‘right back where we started from - RESET’ part.

You could merge feat into main without pulling the new code from main into your feature branch via rebase or merge. This works when your branch is not to much behind main and the changes in main do not alter code your feature relies on.

git switch main
git merge feat
git log main feat --oneline --graph

This would be the history:

*   xxxxxxx (HEAD -> main) Merge branch 'feat'
|\  
| * 707f11d (origin/feat, feat) f1
* | 24d3250 (origin/main, origin/HEAD) 6
|/  
* c350b81 5
* 6f9114d 4
* 3f9e923 3
* c27fadf 2
* 9057d20 1
* ccf0b92 0

All fine, commit 6 stays in the first parent lane.

Questions, comments, ideas?

Just open an issue on the repo or get in contact via mastodon