Merging with Rebase

Before I can explain the concept of resolving a merge conflict during a rebase, I need to elaborate a little about how and why merge conflicts occur.

What happens during a rebase?

The reason we want to rebase, is to move our commits to the top of a TARGET branch. Let’s take this history as a scenario:

* c293e3a updated library `Y` dependency version
* 339ac3c moved and renamed foo.js to src/bar.js
| * 9e538b9 renamed class `Foo` to `Bar`
| * 71c8156 added new external library `X`
| * 448624b `baz` function now uses the validation provided via `X`
|/
* 2b3b44a baz now submits records via XHR

After a rebase we should achieve the following history:

* 9e538b9 renamed class `Foo` to `Bar`
* 71c8156 added new external library `X`
* 448624b `baz` function now uses the validation provided via `X`
* c293e3a updated library `Y` dependency version
* 339ac3c moved and renamed foo.js to src/bar.js
* 2b3b44a baz now submits records via XHR

When rebasing you will see the message:

First, rewinding head to replay your work on top of it...

What it actually does is resetting the HEAD to point to the hash you want to rebase on:

$ git reset --hard TARGET_HASH

Now that the HEAD is the same as your TARGET, all commits that do not exists on the TARGET will be re-applied.
When performing an interactive rebase, you will be presented with a list of commits that will be re-applied:

pick 9e538b9 renamed class `Foo` to `Bar`
pick 71c8156 added new external library `X`
pick 448624b `baz` function now uses the validation provided via `X`

What actually happens are cherry-pick's, picking the commits from the list, top to bottom, one by one:

$ git cherry-pick 9e538b9
$ git cherry-pick 71c8156
$ git cherry-pick 448624b

Which results in a happy case as:

Applying: renamed class `Foo` to `Bar`
Applying: added new external library `X `
Applying: `baz` function now uses the validation provided via `X`

Merge conflicts, HELP?!

There are several types of conflicts. Unfortunately there is no silver bullet. Let’s identify them and resolve them accordingly.

File content conflict

This is the most common merge conflict. In a git merge the end result of the two targets will be presented as one big blob of changes.
There is no information on how the changes got to this point. This makes merging very knowledge heavy.
If you are not involved with the changes causing the merge conflicts, you’ll probably have a difficult time resolving the conflicts alone.

Now git rebase applies commits one by one. This also means you will be resolving conflicts 1 commit at a time.
This gives you a context for the conflict. Our first commit is a rename of the class Foo to Bar.
This means other files might have updated the reference to this class as well. If in the TARGET branch the constructor has changed, you’ll get a conflict.

<<<<<<<<<<<<<
// changes applied by target branch
const foo = new Foo('baz');
==============
// how the code looked like from their common ancestor
const foo = new Foo();
==============
// the changes applied by `renamed class Foo to Bar`
const bar = new Bar();
>>>>>>>>>>>>>>

Now the conflict has a lot of context. Even when looking at these changes for the first time, it is simple to deduct how this conflict should be resolved:

const bar = new Bar('baz');

Let's continue our rebase:

git add
git rebase --continue

The next conflict that occur is with the commit: baz function now uses the validation provided via X

<<<<<<<<<<<<<
// changes applied by target branch
foo.baz(1, 2, value);
==============
// how the code looked like from their common ancestor
foo.baz(1, 2);
==============
// the changes applied by `baz` function now uses the validation provided via `X`
bar.baz(1, 2);
>>>>>>>>>>>>>>

Once again, we have a context around our conflict. We can reason what the result should be.
This demonstrates how commits that are sensible can help resolving conflicts.

Changes already applied

These are conflicts are are easiest to resolve. However the message provided by git might seem scary at first.

First, rewinding head to replay your work on top of it...
Applying: added new external library `X`
Using index info to reconstruct a base tree...
M package.json
Falling back to patching base and 3-way merge...
No changes -- Patch already applied.

Since the changes of this commit are already applied, we can skip this commit all together:

$ git rebase --skip

Often conflicting files

Some files are prone for conflicts. Imagine a package.json dependency that has a dependency that gets updated frequently.

"some-uber-cool-dep": "1.3.44"

It could happen that your branch has the dependency updated, but on the TARGET branch the dependency is even further:

<<<<<<<<<<<<<
// changes applied by target branch
"some-uber-cool-dep": "1.3.44"
==============
// how the code looked like from their common ancestor
"some-uber-cool-dep": "1.3.40"
==============
// the changes applied by
pick 71c8156 added new external library `X`
"some-uber-cool-dep": "1.3.43"
>>>>>>>>>>>>>>

Now we could simply skip this commit, as the changes that would be applied make no sense anymore.

$ git rebase --skip

File moved and renamed conflict

This is probably the most difficult to resolve conflict, a file that you've edited has moved and renamed. The description is also most cryptic to understand:

Merging:
foo.js

Deleted merge conflict for 'foo.js':
{local}: deleted
{remote}: modified file
Use (m)odified or (d)eleted file, or (a)bort?

Git is unable to relate the changes to the same file. We can help git by performing a rename in our branch before our other changes. To do this we are going to first abort the current rebase:

$ git rebase --abort

The first commit where we edited foo.js was 3 commits ago. To edit this commit, we have to point the rebase at least one commit before the to be edited commit.
Git provides a shortcut with HEAD pointing to the current commit and ~n for the amount of commits you want to go back:

$ git rebase -i HEAD~4

We would want to mark the first commit to be edited. Keep in mind, the changes from the following commits will be applied after.
To mark a commit to be edited, change the pick to e:

e 9e538b9 renamed class `Foo` to `Bar`
pick 71c8156 added new external library `X`
pick 448624b `baz` function now uses the validation provided via `X`

The rebase will apply the commit and wait for your changes:

Stopped at 9e538b9... renamed class `Foo` to `Bar`
You can amend the commit now, with

git commit --amend

Once you are satisfied with your changes, run

git rebase --continue

Now rename the file to what it has be renamed to on the TARGET. Now we amend the commit and continue with the rebase:

$ git commit -a --amend
$ git rebase --continue

The following commits will be applied on the renamed file. Now git will be able to associate the changes to the move.
So the rebase on the target should now apply our commits without the cryptic error message!

Another git merge strategy

Intro

There are so many ways on how to utilize git and each has their own advantages and disadvantages. After playing with several strategies I found one that suits my needs and want to share this strategy, to maybe improve or get even more idea’s to an even better suiting strategy.

Rebasing vs Merging

The main goal of both rebasing and merging is to combine different change sets into one. Even though they serve the same purpose, they are still very different. 

Merging

With merging you will create a new commit with the result of the merge between the branches. This merge commit is kept by git as a reference point for the next time git wants to resolve a merge conflict.

Merge Example

git merge

In this example a feature branch is being merged two times back into a master branch. When resolving the first merge conflicts these resolutions will be saved in the first (green) merge commit. When merging the feature branch for the second time, git will recalculate from the first (green) merge commit. The second (green) merge commit will only have to merge the last two (orange) feature branch commits with the (blue) master commits.

Rebasing

When resolving merge conflicts with a rebase, there won’t be a merge commit. Instead the merge conflicts will be resolved on the commits themselves.

Example

before_rebase

Same as with merging we have commits in a feature branch.

first_rebase
When rebasing the changes of the feature branch will be applied on top of master.

before_second_rebase

The next rebase will be applied on top of those changes again.

rebase_result

The end result will be a linear log of changes.

Rebasing for a cleaner log and more

Rebasing will result in a cleaner log, but there are more benefits for rebasing over merging. For instance your merge conflicts will be resolved on commit basis, where as the merge will be one huge change set of all changes combined into one big merge party. Other tools like bisect will also work a lot better, since the commits are actually preserved in a linear history.

For more more awesome info about commit messages, here is an nice post explaining how commit messages can be very usefull: deliberate-git

Feature branches

Imagine working on a new feature. During your development, you notice there is a bug in the code you would want to fix before finishing your new feature. We can simply create a new branch, branched from the stable master and apply this bug fix. Now we switch back into our local branch containing the changes we made up until we found the bug and continue. How awesome is that?

Applying the bug fix, could cause you to not being able to finish your feature on this day, but you still want to share your code with the rest of your team. You can’t push this to the master, as you want only stable code in the master branch… No worries, we can push the commit to a Feature branch!

Pausing/Dropping

An additional advantage would be when problems occur while developing this feature, either from business side or from technical side, the feature can be paused or even dropped at any time. Preventing risks of having a release of other features postponed.

Testing

We only want stable code in our master branch and Feature branches can help us achieve that. The feature can be tested isolated from any other parallel developed feature.

Local pulling

Ideally we would want even stable code in the feature branch, which would allow test suites to be run at every commit in the feature branch for even more feedback. However in the previous example, a commit was pushed which would break tests.

Pulling from a local machine is one of the solutions which I prefer. In order to pull from a local machine, the developer hosting the files, must start the daemon first. This forces awareness. When you are pulling from a local machine, you are more aware of the fact that you might actually pull something in which is not stable.

before_local_pulling

This allows for even tighter collaboration between the developers. Being able to commit not working changes, allows smaller commits which would not work separately but would work when combined with the commits of the other developers. 

rebase_local_pull

After pulling and finishing the changes on a local machine, we can squash the commits into one working commit and share this in the feature branch to allow QA to test these changes. (Note: Squashing commits means rewriting those multiple commits and commit them as if they were one commit)

squashed_commit

After QA has tested and approved the feature, we can squash the bug fixes into grouped commits. Don’t simply merge all commits into one commit, but merge them into sensible commits.

Knowledge Sharing

By working on a feature with such small tasks, each team member learns more about the feature being implemented. Instead of having three features implemented separately by three developers, the three developers could develop a bit of each feature instead, spreading the knowledge of the feature as well as having the feature earlier testable.

Conclusion

Cleaner log + applying commits one by one = resolving conflicts more easily

With the help of rebasing, we create a log history that is easy to read. Rebasing applies the changes commit for commit on top of the changes made in an other branch.

Parallel development + Feature branches = Flexible release scope

With the help of feature branches we can group commits together and squash  them into sensible commits to keep the log even more clean. Features won’t be blocking each other from being released anymore and if needed they can be dropped entirely.

Sequential Feature development + local pulling = More knowledge + Earlier testable feature

With the help of pulling from local repositories we can work even more closely on features. Sharing more knowledge among the team and preventing QA from having to test all features at the end of the sprint!