Background
This interactive tutorial about the resolution of conflicts in git is a continuation of my first blog in the series titled And then we resolve a merge conflict.... If you have not read it, please check it out here.
The aim of this tutorial is to show how you can use rebasing as an alternative to merging so to combine to branches in git. It will demonstrate how you resolve conflicts that occur thereof.
Intro to rebasing
Every commit in github is based on its preceding commit. When you trace the history you realize that every commit has an ancestor.
Practically this is shown by that whenever you log the history of a branch, all the commits are listed in chronological order. With first commit at the bottom and the most recent commit which is the HEAD at the top.
Now, what rebasing does is it rewrites history of your current branch, through assigning a commit of your choice as the ancestor of the HEAD.
Now, let's get started
As indicated in the first blog to get started, we create a repo on github and initialize it with a README. Then clone it locally on my machine. After that, I open the project on an editor. My editor of choice is Visual Studio Code.
I then create and check out a new branch called list-interests. And create bio.txt. In bio.txt I add a list of my personal interests. Then voila:
To stage the changes, on the terminal I run the command,
git add .
Then to commit the changes to my local repo I run the command,
git commit -m "Adds interests to bio"
Then to look up the history of the list-interests branch, on the terminal I run the command;
git log --oneline
The result:
5d944ac (HEAD -> list-interests) Adds interest to bio
305d937 (origin/master, origin/HEAD, master) Initial commit
Now let's imagine that a bunch of devs was assigned to this project. One dev, in particular, was assigned to add an intro to the bio. The dev successfully created the intro and then proceed to merge it to master.
To mimic that, on the terminal run the command,
git checkout -
The command above listed above allows you to navigate to the branch you were in before you checked out your current branch.
On master I then create the file bio.txt. Then add a basic intro about myself.
I then add the changes to the staging area by running the command below on the terminal.
git add .
To add the changes to my local repo,
git commit -m "Adds intro to bio"
Then I log the history by running the command;
git log --oneline
The output:
4586948 (HEAD -> master) Adds intro to bio
305d937 (origin/master, origin/HEAD) Initial commit
Then to sync master to my origin/master which is the branch that corresponds to master on the remote repository;
git push
I then check out the branch list-interests to continue.
If you log the history of the branch list-interests you will realize that it was based on the old version of master that did not have the intro. I intend to rebase it to the new version of master so as to include the intro.
To do that on the terminal I then run the command;
git rebase master
On the terminal I get the error message:
First, rewinding head to replay your work on top of it...
Applying: Adds interest to bio
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
CONFLICT (add/add): Merge conflict in bio.txt
Auto-merging bio.txt
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch' to see the failed patch
Patch failed at 0001 Adds interest to bio
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
The above message informs me that git failed to merge then provides me commands I can use to resolve the issue.
On the editor, this is what is there:
Because I want to keep both the changes, I select the option Accept Both Changes on VS code.
To continue I then add the change to the staging area, by running the command;
git add .
Then on the terminal, I run the command;
git rebase --continue
Then get the message
Applying: Adds interest to bio
Under the hood when you rebase, git basically rewinds fetches the tip of master, places it in the position of the last commit of the branch you had checked out, then applies the tip of list_interests on master.
To visualize it imagine your history was a deck of cards, a stack. You pop the topmost card which is the HEAD of your current branch, in its place you push the HEAD of master then finally you push the HEAD of the current branch in its rightful position.
If you have conflicts you resolve them and then stage the changes. But instead of making a new merge commit when you run the command git rebase --continue the changes are applied to the HEAD, which in this case had the message Adds interests to bio. When you log the history you get:
1a378f3 (HEAD -> list-interests) Adds interest to bio
4586948 (origin/master, origin/HEAD, master) Adds intro to bio
305d937 Initial commit
I have successfully made a rebase. Rebasing offers a clean way to combine diverging branches and its main advantage is the history is kept linear, unlike when you do merge commits.
I then sync my local and remote. Make a pull request. Then merge my pull request to origin/master. On my local I checkout master then on the terminal run the command;
git pull
So as to sync master to origin/master. Then voila:
With that done, hopefully, you now have an understanding of how rebasing works in git. We now proceed to the main business of the day which is using rebasing to implement what we did on the first blog in the series.
Well, I would love to add a new feature to my bio. This time I will add a list of programming languages I have worked on. To do that I create and check out a new branch called list-languages. On the terminal, I run the command,
git checkout -b list-languages
Then I edit bio.txt by adding a list of programming languages.
To proceed I then add the changes to the staging area by running the command;
git add .
To commit the changes to my local repo, on the terminal I then run the command;
"git commit -m "Adds programming languages to bio"
To create a branch on the remote corresponding to list--languages and push changes associated with it. On the terminal I then run the command;
git push --set-upstream origin list-languages
I just realized I made a blooper on my bio. Instead of adding C++, I meant to add C. But in the same regard, I want to keep my commit message.
So back on my editor, I edit my bio.
Now to stage the changes I run:
git add .
And to commit to my local, on my terminal I run the command:
git commit --amend --no-edit
As we know from the first tutorial git commit --amend --no-edit, alters the history of the branch. We end up with a scenario whereby the local and remote diverge from each other with the remote branch always leading.
When I attempt to sync my local and remote using:
git push
I get this message as a response:
To github.com:JackieBinya/git-tut-2.git
! [rejected] list-languages -> list-languages (non-fast-forward)
error: failed to push some refs to 'git@github.com:JackieBinya/git-tut-2.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Before we proceed let's take a quick look at the history of the branch list-languages, using the command:
git log --oneline
The output:
c0fa933 (HEAD -> list-languages) Adds programming languages to bio
d2c5e0f (origin/master, origin/HEAD, master) Merge pull request #1 from JackieBinya/list-interests
1a378f3 (origin/list-interests, list-interests) Adds interest to bio
4586948 Adds intro to bio
305d937 Initial commit
Basically when I run the command git push. Under the hood git attempts to sync my local list-languages to the corresponding branch on the remote origin/list-languages. Git is clever enough to realize that the branches have diverged, the remote is ahead of the local branch so it suggests I pull changes on the remote to my local, resolve any conflicts and push again.
To resolve the issue highlighted I run the command:
git pull --rebase
I added the flag --rebase to the git pull command so as to rebase the HEAD of my local on the tip of origin/list-laguages. This offers an alternative to merging branches.
On my editor this is what my file looks like:
To resolve the conflict, on VS Code I select the option to keep incoming changes. I then add the changes to the staging area then run the command;
_git rebase --continue_
This is what bio.txt looks like on my editor:
When I log the history on the terminal I get the output below:
b584e1c (HEAD -> list-languages) Adds programming languages to bio
7b06700 (origin/list-languages) Adds programming languages to bio
d2c5e0f (origin/master, origin/HEAD, master) Merge pull request #1 from JackieBinya/list-interests
1a378f3 (origin/list-interests, list-interests) Adds interest to bio
4586948 Adds intro to bio
305d937 Initial commit
I then sync my local and remote. Make a PR then merge it to master.
Finally my bio:
Thank you for reading my article.
Top comments (3)
Great article!
Additional tip: the first thing I do when I configure git in a new environment is to change the conflict markers:
It turns this:
into this (Case A):
or this (Case B):
The middle section (“merged common ancestors”) is what was there before each side’s change. So you can see better what happened.
In Case A, we see that the change on
master
was to correct a typo, and the change onlist-interests
was to replace “art lover” with “paintball master”, and to add the list of interests. So solving the conflict in a way that preserves the intents of both sides means:In Case B, however, the change on
master
was to replace “rocket scientist” with “art lover”. while the change onlist-interests
was to replace “rocket scientist” with “paintball master” and add the list of interests. So solving the conflict in a way that preserves the intents of both sides probably means something like:Or it might be needed to consult with the other person to decide whether “rocket scientist” is better replaced with “art lover” or “paintball master”.
With the default config of
merge.conflictstyle = merge
, you get the same output for the conflict in both cases, and it’s much harder in my opinion to figure out what was the intent on both sides.This simple config change made conflict resolution much less of a headache for me.
Thank you loads.
Your suggestion is really dope and indeed as you said if you can see the initial state before the changes, resolution becomes way easier.
I will definitely start implementing this approach.:)
This is great!!