Intro
A couple of days ago I was caught in a vicious multilevel rebasing cycle, I have noticed that I faced the same issue before, so I decided to track down what caused the mess to begin with. Now I think that what I found is worth sharing.
The Issue
Now the core issue is changing the parent/head/base branch of some branch from one to another. It is already obvious that this would cause a mess, but understanding this mess would help us deal with it, either by fixing it or mitigating it.
Step by Step
Step 1
Lets consider that you have two branches Master and Feature 1. Where master is root of the tree and the feature branch is based on master and has one extra commit.
Step 2
Now lets consider having two more commits on master.
Step 3
Then we create a new feature branch from master.
Step 4
Now we realize that we want feature 2 to be based on feature 1 instead of master, so we apply a rebase of feature 2 on feature 1.
Quick note on how a rebase works (this is not an accurate
full description but just a way to think about it that is relevant to our current context): A rebase applies all the base branch commits then checks every commit in the current branch, if the commit hash is already found in the base it skips it, else it applies it on top as a new commit with a new hash.
In our diagrams here I’ll add a number to a commit name to denote the creation of the same commit with a different hash.
We can consider this to be our first mistake, this rebase caused commits “E” and “F” to be duplicated where the same content is also found in commits “E2” and “F2”.
Step 5
Moving forward things start to be really messy, lets consider that we decided to rebase feature 1 on master to get the latest changes and then add a new commit “H“ after the rebase. As you can see bellow, commit “D“ is also duplicated into commit “D2“.
Step 6
The grand finale, we now top it off with our need to rebase feature 2 on feature 1 again to get commit H into feature 2. We are not realizing that we will be now getting all duplicate commits together in one branch. This step marks our second mistake.
Why is This an Issue?
What happened here caused two main problems:
- Our history now contains duplicate commits which is very unclean.
- The rebasing process becomes very tedious and confusing, since we keep seeing conflicts with commits that are deep in our history and we shouldn’t be facing them again, and we would probably end up doing more mistakes while rebasing.
How Do We Overcome This?
Now there are two solutions, the first avoids the issue from the beginning. Lets say we already need to change the head/base branch from one to another.
The first thing to do would be to avoid mistake number 1 that was made in step 4 in the above diagrams. We should be using an interactive rebase where we cautiously drop commits “E2“ and “F2“ from the rebase, i.e. we only keep commits that were actually created on feature 2 branch (commit “G2”).
Now say we missed doing the previous fix, then our second option would be to avoid mistake number 2 that was made in step 6. Again we need to use an interactive rebase where we cautiously drop commits “D3“, “E3“, “F3“, i.e. we only keep commits that were originally created on feature 2 branch (commit “G3”). Unfortunately now its much harder to figure out exactly which commits need to be kept since the Pull/Merge request would probably be already messed up with commits “D“, “E2“ and “F2“.
Conclusion
Just make sure to use the correct branch from the beginning 😂. Of Course having a well thought out branching strategy to begin with helps a lot in avoiding such issues.
Top comments (1)
Better ways to do this:
1) Create a new branch forked from feature1 then cherry-pick all commits from feature2 after its fork from master (in your diagram, a single commit), in a line.
2) Standing on feature2, use "git rebase --onto feature1 master" - this will, as in (1), move all commits after master on top of feature1.
3) Standing on feature2, use "git rebase -i feature1" and manually delete all commits except the sequence standing from master.
Of course, be ready to resolve conflicts. Use visual tools if needed. Always set merge.conflictstyle=diff3 to export common base to such tools (I don't understand why it's not default).
Often it's impossible to prophesy such things. So just be ready to handle such a weird situation as well.