Git rebase conflicts optimisation

81 views Asked by At

Lets say I have created a branch F1 from main. This F1 branch has 20 commits in it. I now want to rebase this branch onto main to pick up changes from main

git checkout F1
git rebase -i main

However, I know that almost all the commits on F1 modify a file that has also been modified on main. So in almost all of the 20 commits I am going to have to resolve conflicts.

It seems like a waste of time fixing a conflict in commit 1/20, just for it to then also conflict in 2/20 and 3/20 and so on.

Should I instead rebase F1 from its first commit (1/20) and squash all of the 20 commits into 1? and THEN do the rebase onto main?

i.e something like :

git checkout F1
git rebase -i HEAD~19 (add -squash to the last 19 commits)
git push -f

then

git checkout F1
git rebase -i main
2

There are 2 answers

2
TTT On BEST ANSWER

We have some excellent comments on the question, which I will credit and summarize here into an aggregate answer.

Should I instead ... squash all of the 20 commits into 1 ... and THEN do the rebase onto main?

As NoDataFound mentioned in a comment:

I would say it depends on the importance of these commits in the history!

If you are willing to squash...

As Axnyff mentioned:

I usually do a squash then a rebase but you shouldn't do it if you have meaningful commits.

If you were planning on eventually squashing your 20 commits into fewer "good" commits anyway, then yes, absolutely do that squash first and then the rebase. If you probably weren't going to squash them, but it doesn't bother you that much if you do, then again, you might as well squash first. Note that if you're using a Pull Request tool that tracks the history of a branch in the PR, then consider creating the PR before rebasing, and then force push your branch after the rebase so that the 20 commits can still be found in the history of the PR. This is one way to preserve history in the PR tool, even though you won't have the history in the repo itself. I do this sometimes when I suspect someday in the future I may wish to see more granularity of the individual commits, perhaps for determining the reasoning behind why a certain change was made that may be harder to determine in a larger squashed commit. Even without the individual commits in the repo, it's easy to go back and look at the PR to see the original list of commits which you could investigate or even checkout locally again.

If you don't want to squash...

If your commits are important enough to keep, even though a linear history is nice, sometimes the benefit of having a linear history is outweighed by the pain to achieve it. This may be one of those cases where you decide to accept a merge commit. As Tim Biegeleisen points out:

In fact, the easy way out of this would be to git merge instead. A merge would yield a single event with all conflicts in one go.

That is by far the simplest solution, and even in a strict rebase workflow, perhaps an occasional merge commit from bringing in main wouldn't cause too much angst.

If you don't want to squash and can't have a merge commit...

If you want to keep your full history and also not have a merge commit, then you'll have to deal with the conflicts. There are some options which may help make the process more efficient. From LeGEC's comment:

git rerere can help: git config --global rerere.enabled true. See git help rerere and the git book for more detailed explanations

Another option is to try using the -X option for resolving conflicts. This will attempt to resolve the conflicts by favoring either "ours" or "theirs". Note: when rebasing, the meaning of "ours" and "theirs" are flipped compared to when merging. If you are merging main into F1 then you will have F1 checked out, and as expected "ours" is F1 and "theirs" is main. However, when rebasing F1 onto main, then during the rebase main is "ours" and F1 is "theirs". For example, if you wish to resolve the conflicts by keeping the change on main for all the conflicts in the 20 commits on F1, you would use:

git rebase main F1 -Xours

Note when rebasing you'll also see the terms "incoming" and "current" which are similar to "theirs and "ours". More info here.

2
hlovdal On

I now want to rebase this branch onto main to pick up changes from main

NB the command git rebase -i main will not do that! The syntax you are looking for for doing that is

git rebase main F1     # Rebase F1 on top of main (independent on what the current branch is).

I will strongly recommend that you keep all your original 20 commits when doing your rebase.

Yes there are then 20 theoretical conflict points, however a really important point is also that smaller commits are less likely to generate conflicts in the first place!

If you have commits like for instance "Remove unused imports" and "Refactor: extract method foo", then the import commit is less likely to generate a conflict on its own with fewer lines changed compared to when it is combined with some other code that perhaps also adds additional imports so that many of the import lines are changed at once.

And for the refactor case let's say you get a conflict because the code you extracted to foo has been modified on main then resolving that is trivial because you just copy all the relevant lines from the main's version and replace whatever F1 was contributing for this refactor commit. This is not trivial to resolve if you have combined the extraction together with additional modification of the code inside the foo function in a commit.

So by keeping small commits you reduce the probability of conflicts, but if you get there are remedies to lessen the burden of resolving. For instance you can use KDiff3 to resolve the conflicts which normally automatically resolves changes on neighboring lines as long as they do not change the same line.

And even for same line modifications KDiff3 is a full fledged 3-way merge program that makes resolving conflicts a superior experience compared to not using a proper 3-way merge tool.

It seems like a waste of time fixing a conflict in commit 1/20

You are fearing what I would consider a largely imaginary problem. There is no conflict that re-appear and re-appear in absolutely all the commits (and should there be something that re-appears more than once there is rerere as already mentioned (I have no experience with using it)). Again I would argue that smaller commits is the best tool in avoiding conflicts and that keeping all your original commits is the way to go.