Troubleshooting Team Conflicts with Git Branches
We comprehensively discussed the differences of rebase and merge within Git, but we didn’t quite hit on the challenges. When you’re working with a team of developers accessing the same master, you’re bound to have conflicts that need to be resolved.
How to Fix Changes to a Shared History
There are a few valid reasons to do so, but it’s almost always a bad idea to change shared history. The universe being what it is, if a process allows for the existence of a problem, it probably will happen at some point, so we have to be prepared for the event in which someone changes shared history. We keep knives and sharpeners in the kitchen, but we also keep bandages.
Good team communication can mitigate most of this, so if you are struggling with this problem frequently, you probably want to address that rather than try to disallow the usage of the rebase strategy.
Let’s say your teammate notified you that they would rebase a feature branch to clean some things up. If you have any outstanding changes that you have not pushed up the branch yet, let them know this and if possible try to get your changes in there before the rebase is done. If you don’t have any changes in the wings, consider taking a short break from changes to allow your teammate time to clean up the branch.
When you find out that a rebase has been done and you don’t have any orphaned commits, the fix is quite easy. We use the reset command to tell git to make our local repository look like the remote repository that was just changed:
$ git fetch $ git checkout feature-branch $ git reset --hard origin/feature-branch
Sidebar: Fetch, Don’t Pull in Git
Astute readers may notice that we used the git fetch command above. This is because git pull is actually a combination of git fetch and git merge. As we’ve pointed out numerous times, we prefer to keep separate things separate so we’d rather not do both simultaneously. If it were up to us, we’d probably remove the git pull command altogether, because it can be extremely dangerous if not understood well.
It’s a good idea to get in the habit of only using fetch. For teams using Gitlab or similar this makes even more sense because they do all their merging through the web UI, so there is never a need to do both fetching down new commits and merging them in a single step. The fetch command is also great for doing code reviews and other things where you aren’t yet sure that you want to merge the code, but that is beyond the scope of this series.
The –hard option tells reset to be ruthless in forcing your current branch and the working directory to look exactly like the specified branch. In this case, we’re telling git to make our local feature-branch look exactly like the remote feature-branch which is denoted by adding the remote name origin/feature-branch.
It’s important to understand that if you have any local commits on your local branch or non-committed changes in your working directory, those changes will be removed when you perform a reset. This is the worst case of dealing with time traveling: unpredicted timeline changes.
When Reset Isn’t Enough
When a reset would remove commits you don’t want removed, the first thing to do is communicate with your team to ensure it was intentional. Since these tools are so sharp, it’s worth commenting to your teammate that you found blood, as they may not have noticed that they cut themselves.
Returning to our add-7 example from above, if we tried to merge the add-7 branch to the master now, there would be a conflict we would have to resolve. If ‘Add 6’ was a simple change, maybe it’s easy to resolve the conflict, but we’d rather avoid that hassle. Instead, we can use rebase to just remove the extra ‘Add 6’ commit, which will remove the conflict.
$ git checkout add-7 $ git rebase -I 37432bc pick 7c8bea0 Add 6 pick ed0c134 Add seven pick 0f5d81c Make it Seven pick c516341 Simplify to 7 # Rebase 37432bc..c516341 onto 37432bc (4 commands) # # Commands: # p, pick <commit> = use commit # r, reword <commit> = use commit, but edit the commit message # e, edit <commit> = use commit, but stop for amending # s, squash <commit> = use commit, but meld into previous commit # f, fixup <commit> = like "squash", but discard this commit's log message # x, exec <command> = run command (the rest of the line) using shell # b, break = stop here (continue rebase later with 'git rebase --continue') # d, drop <commit> = remove commit # l, label <label> = label current HEAD with a name # t, reset <label> = reset HEAD to a label # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>] # . create a merge commit using the original merge commit's # . message (or the oneline, if no original merge commit was # . specified). Use -c <commit> to reword the commit message. # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out
This time, instead of the squash command, we just want to remove a commit. As the comments show, this is done by simply deleting the line with the commit we want to delete.
pick ed0c134 Add seven pick 0f5d81c Make it Seven pick c516341 Simplify to 7 # Rebase 37432bc..c516341 onto 37432bc (4 commands) # # Commands: # p, pick <commit> = use commit # r, reword <commit> = use commit, but edit the commit message # e, edit <commit> = use commit, but stop for amending # s, squash <commit> = use commit, but meld into previous commit # f, fixup <commit> = like "squash", but discard this commit's log message # x, exec <command> = run command (the rest of the line) using shell # b, break = stop here (continue rebase later with 'git rebase --continue') # d, drop <commit> = remove commit # l, label <label> = label current HEAD with a name # t, reset <label> = reset HEAD to a label # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>] # . create a merge commit using the original merge commit's # . message (or the oneline, if no original merge commit was # . specified). Use -c <commit> to reword the commit message. # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out
So now it seems like we’re good to go. We have a master branch with the correct ‘add-6’, and we have an ‘add-7’ branch that doesn’t contain that conflicting original ‘add-6’. However, we haven’t really fixed it yet because each of the commits to add 7 relied on the fact that the new entry was added after ‘66’.
Git stores diffs, it doesn’t actually understand the changes you’re making. So now if you try to merge the ‘add-7’ branch, you’ll find that there are still conflicts. First, it will ask you to correct adding ‘seven’ after ‘66’, then it will ask about adding ‘Seven’ after ‘66’. The conflicting diff will look something like this:
0 1 2 3 4 4.5 5 66 Seven
This becomes a bigger pain the more commits you have after the correction. You’ll have to fix each commit up the chain by effectively applying the same patch to each subsequent commit. With this trivial example that’s not too bad, but as soon as you have code that requires thought it’s easy to screw up such a repetitive task. So, let’s abort the merge and let git help us out again.
Rerere: Automated Repetition
Git has a handy tool for helping with repetitive patch applications for cases like this, but it is disabled by default. There are probably some cases where enabling this tool is problematic, but beyond slight performance hit we’ve not run into them, so that is beyond the scope of this article.
The command is called ‘rerere’, which stands for “Reuse recorded resolution”. Enable it by editing your .gitconfig file (usually in your home directory) to add rerere.enabled = true.
After a conflict is found on a merge or rebase, you can use git rerere to record the state of the conflict. Then once it has been resolved, git rerere will record the resolution. The power comes in when we’re resolving a string of conflicts that all need to be resolved in the same way, like to our add-7 problem above where we need to change ‘66’ to ‘6’ in a bunch of commits so that the diffs can find where to make the changes. If we let git rerere record the changes we make while resolving the first conflict, we can then let git reuse those changes on subsequent commits.
The best part is that git doesn’t even force you to type git rerere at the appropriate times because it just automatically runs it, if its enabled, anytime it runs into a conflict that could be resolved by the reusable patch. This allows us to resolve this entire add-7 conflict chain by going all the way back to when it first was noticed and then just doing a single merge:
$ git co master Previous HEAD position was c516341 Simplify to 7 Switched to branch 'master' $ git merge add-7 Auto-merging file CONFLICT (content): Merge conflict in file Recorded preimage for 'file' Automatic merge failed; fix conflicts and then commit the result. $ emacs file $ git add file; git commit -m “Merge add-7” Recorded resolution for 'file'. [master 3cf4e41] Merge add-7 $ git log $ git r * 3cf4e41 (85 seconds) <Dave Strocl> (HEAD -> master) Merge add-7 |\ | * c516341 (3 months) <Dave Strocl> (add-7) Simplify to 7 | * 0f5d81c (3 months) <Dave Strocl> Make it Seven | * ed0c134 (3 months) <Dave Strocl> Add seven | * 7c8bea0 (3 months) <Dave Strocl> Add 6 * | 707ae1b (3 months) <Dave Strocl> Add 6 |/ * 37432bc (3 months) <Dave Strocl> Add five correctly the first time
Notice the new line “Recorded preimage for ‘file’”. This is git rerere telling us that it stored the current state of the conflict so that it can determine the resolution. Then, once we’ve resolved the conflict and committed it, we see “Recorded resolution for ‘file’” which is git rerere recording the resolution.
What it doesn’t show you is that it used that stored resolution to resolve merging in the final 2 commits (0f5d81c and c516341) without your involvement. This almost completely removes one of the biggest cautionary results of using the rebase idea.