You may not be able to avoid all merge conflicts, but planning and communication can take you a long way. You can avoid conflicts in the first place by scheduling and assigning work on user stories and project tickets in ways that are less likely to cause conflicts.
While we still frame and prioritize user stories from the user’s perspective, stories that involve the same bits of code may need to be worked in series. This way, you can safely work other stories in parallel, without causing problems. Breaking up large tasks into smaller ones can also reduce conflicts and increase parallelism.
In cases where your engineering team does need to work related stories at the same time, communication is key. By sharing an outline of your intended approach before starting, you can get valuable feedback and allow others to plan around changes. Sharing WIP branches frequently allows everybody to stay in sync as work progresses. This makes it easy to resolve any conflicts that do arise.
Get familiar with your tools
First, configure your git mergetool settings and get very comfortable with the interface of that tool. This is the most important thing, if you haven’t done it yet. Some tools use a three-column layout, with theirs on the left, yours on the right, and the conflicted version in the middle. To resolve the conflict, you edit the middle file, and the diffs in each file constantly adjust so you can see how your merged version differs from each of the parent commits. Lately, though, I’ve been using VSCode, which only shows the “middle” file with the conflict markers. It does add some helpful shortcuts to speed things along.
A three-column merge view in Meld:
Simple conflict markers in VSCode:
Here’s the relevant section of my
.gitconfig, for using VSCode as a merge tool:
[merge] tool = vscode [mergetool] prompt = false keepBackup = false # see below under Cleanup [mergetool "vscode"] cmd = code --wait $MERGED
Whatever you use, get comfortable resolving conflicts. Handling a nasty one on a real project under a tight deadline is no fun. So, it’s worthwhile to intentionally create some merge conflicts and practice resolving them in various ways. It does take a bit of time and energy.
In addition to getting more familiar with the tools, this process also helps you understand what kinds of changes will cause conflicts. In turn, this will help you to anticipate and deal with real-life conflicts. Finally, when facing down a potentially tricky conflict on a real project, you can gain confidence by running through your merge (as many times as you like) on a temporary throwaway branch first.
Git is your escape hatch
Git has your back, and there’s pretty much always a way to reset your repository to a previous, non-broken state. If you haven’t committed your merge, you can give up and go back to your pre-merge state with
git merge --abort. If you have committed the merge already, and you need to undo it, you can still rewind with
git reset (HASH_OF_PRE_MERGE_COMMIT) if you haven’t pushed your bad merge anywhere, or
git revert (HASH_OF_MERGE_COMMIT) if you’ve already shared it.
If you’re working your way through the conflicts in a particular file and realize you’ve done something wrong, you can start over with that file using
git checkout -m -- FILE_PATH. This command will give you back all of the original conflict markers in that file. This way, you can revisit your decisions.
Current, Incoming, or Both
There are a couple of common cases I run into when merging code changes that interleave with those of my teammates. If I’ve done a good job of staying up-to-date with what my teammates are working on, I often know ahead of time that either my changes or their changes are the “right” ones in a particular file.
For me, these cases fall into one of two patterns:
- If I’ve put in a quick hack around bug unrelated to my current work, but now want to incorporate the ‘real’ fix exactly as it exists in the upstream branch, I probably want to ‘Accept Incoming’ for every change in that file.
- If I’ve been careful in my branch to faithfully preserve the behavior from the other branch where they overlap, I may want to ‘Accept Current’ for every change in a file.
My shortcut for this situation used to involve a bit of complicated git plumbing (e.g.
git show :3:path/to/file > path/to/file), but I’ve recently discovered that VSCode has built-in commands like
Merge Conflict: Accept All Incoming for exactly this situation. The main trick here is making sure you’ve got the right understanding of what ‘Incoming’ and ‘Current’ mean for the current merge. Sometimes these are also called “ours” or “mine” and “theirs,” and they can get especially confusing during a rebase.
Sometimes after merging, you’ll see extra files with the
.orig extension hanging around in your repo. You can have git clean these up automatically by setting
false in your git config (see above), or you can manually clean them up using something like
find . -name \*.orig -delete.