Resolve Conflicts

Last update: 29 May 2024 [History] [Edit]

As we discussed before, in our GitLab Flow workflow we do not advise worrying about re-syncing (merge or rebase) with the upstream code for small developments that you can do in isolation (because they are fast or because you are the only developer working on that package). When the time comes to merge your changes, git is smart enough to only take your changes into upstream and it matters not at all if other files you did not work on changed in the meantime. (Actually, even if files you were working on did change merges are possible if only different parts of the file were changed.)

For larger, longer-lived developments, rebasing periodically (for example, at the start of the day but latest when you are ready to push your branch) is a very good idea, ensuring that you do not get too out-of-sync with the HEAD of the branch you want to merge to. In case there were changes upstream that conflict with your own changes, you will have to resolve these.

Resolving conflicts by rebasing

This is the recommended procedure to resolve conflicts:

git checkout [mybranch]
git fetch upstream
git rebase upstream/main
# Fix any conflicts
git push --force origin

What is effectively happening here is git is making your local branch point to the HEAD of main, and then replaying your commits on top. If there are conflicts, the affected files are marked up with the usual conflict indicators (>>>>>>>) indicating which pieces of the file came from which versions. As the developer you now need to decide what the correct solution is (StackOverflow has lot of useful guides to understanding the markers, and github has a nice step-by-step). For each resolved conflict, git add the file(s) and then continue the rebase with git rebase --continue.

If your branch had already been pushed to GitLab before the rebase, you will need to --force push as the last step (because the rebase rewrote its commit history).

If some merge conflict resolution is going badly wrong then you can abort a rebase in progress with

git rebase --abort

This would also allow you to restart with a different conflict resolution strategy.

Moving a Merge Request between branches

If you created your local branch off of the wrong upstream commit, for example you started your work from the HEAD of main but actually wanted to start from the HEAD of 24.0. You can use rebase just as described above to replay your commits onto the correct branch. The only difference is in the third line where you specify the branch you want to rebase onto in addition to the branch you originally branched off from.

git checkout [mybranch]
git fetch upstream
git rebase --onto upstream/24.0 upstream/main
# Fix any conflicts
git push --force origin

If you already have an open MR that needs to be moved between branches, proceed as follows:

  1. Mark the MR as “Draft” to avoid accidental running of the CI
  2. Remove all release labels (e.g. 24.0, main)
  3. Follow the rebase --onto procedure described above and force push
  4. Change the target branch of your MR (“Edit”, select new branch from drop-down, “Save changes”)
  5. Verify that the commit history and changes look as expected
  6. Remove the “Draft” status

Manual squashing in branches with many conflicts

If you repeatedly merged your branch with the target (e.g. main) and resolved conflicts, the automatic Gitlab commit squashing may fail, requiring you to manually squash your commits into one. When possible, this should be done with interactive rebase. However, in situations where you have many commits to squash, rebasing will require you to solve the conflicts again for every commit, which may be more error prone. In this case, you can reset your branch with respect to the target, which will retain the diffs locally but remove them from the commit history, allowing you to make a fresh commit.

To ensure that you don’t lose work, you can do the following (commands below):

  1. Check out the development branch if you are not already on it (replacing [mybranch] as appropriate)
  2. Copy your development branch as a backup
  3. Reset the commits on your branch with respect to the target (replace [target-branch] below with e.g.upstream/main)
  4. Check that the diffs are correct against your backup branch
  5. If everything is OK, commit with a sensible commit message.
  6. Force-push to the remote
git checkout [mybranch]
git branch -c [mybranch]-backup
git reset $(git merge-base [target branch] $(git rev-parse --abbrev-ref HEAD))
git diff [mybranch]-backup
git commit -a
git push --force origin [mybranch]

If at step 3 you find that the diffs do not match, do not proceed! You may have to check the target branch, update your development branch further, etc. Once you have successfully reset and squashed the commits, you can delete the backup branch.

See this StackOverflow thread for more details.