Imagine a castle made of git!
Wait a second.
That's not the sentence I wanted to start with. Anyway.
The problem
If you have ever worked together with a team on a project in git, you might have ended up working on a chain of commits. All lining up nicely, ready to be pushed to the common remote repository that sits behind your team's gihub/bitbucket/gerrit/whatever server.
This is when one of your colleagues would say something like this:
"Hey Caren, you forgot to add a semicolon in that file. You know the one you added in the middle of your commit chain."
And this is where you start checking-out and rebasing and possibly end up in a merge conflict hell.
If you don't really understand what's going on here this article is for you!
Let's discover the problem with a very simple example. This is where the castle comes to the picture.
Meet your git castle!
We will create our git castle as simple as this:
mkdir git-castle
cd git-castle
git init
touch castle.yaml
As you might have thought our git castle will be just a file, representing a piece of code that we will edit and make commits with until we properly suffocate ourselves with merge conflicts.
Let's create our code representation, as a yaml file, describing parameters of our castle. We will also create a chain of commits to work with.
Our code representation file:
# This is my git castle!
---
location: Camelot
built: 2010-10-10
owner: Penny Bunn
rooms:
- room_name: Living Room
- room_name: Kitchen
Let's create our initial commit with adding this file.
git add castle.yaml
git commit -m "Initial Commit"
Now we add some parameters, and create a second commit as "Add room parameters":
# This is my git castle!
---
location: Camelot
built: 2010-10-10
owner: Penny Bunn
rooms:
- room_name: Living Room
size: 30m2
windows: 2
doors: 2
- room_name: Kitchen
size: 15m2
windows: 2
doors: 1
Now we add another room, and a third commit named "Add bathroom"*:
- room_name: Bathroom,
size: 10m2
windows: 1
doors: 1
Cool! We have a three commit long commit chain!
Going back to fix something
But no! We suddenly learn, that according to our plans the living room should have 3 windows!
Naturally, it would be nicer to fix this on the original "Add room parameters" commit. Especially now, that we noticed our mistake in the commit chain before pushing it to the remote master.
This is where interactive rebase comes to the picture.
In git, the
rebase
command is used to reapply commits to a chosen base tip. Luckily it has an interactive flag, and with it, you can simply go back to a chosen commit, edit it, and reapply all the following commits till master.
git rebase -i ${base_tip}
As a base_tip
we will choose any commit that is older (further back) than the point we want to edit. When you have a remote repo, that point will be the remote master, which will also be the base of your commit chain, so best to rebase on that. Now in our case we will just use the hash of our initial commit.
We can figure that out with
git log
orgitk
, orgit rev-parse HEAD~$num
where $num is our distance from the initial commit.
After smashing enter we will be greeted with an interactive editor window. We will see all the commits above our chosen base_tip
and we can choose which commit to edit by simply replacing the chosen pick
with an e
. Like this:
e ddbef6b Add room parameters
pick 607bd93 Add bathroom
# Rebase cf0e9f4..550ab2c onto cf0e9f4 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
If you have never seen these interactive windows either here or when committing and you also don't know the default editor of git (emacs or vim) you may have some trouble how to edit and save the document. You might want to look into vim/emacs if this is the case.
After the successful edit, we can check with gitk
or git rev-parse HEAD
that we are indeed at the commit which we wanted to edit. We will do our fix like this:
rooms:
- room_name: Living Room
size: 30m2
windows: 2
doors: 2
With git add
and git commit --amend
we can add the change to the "Add room parameters" commit. After this, we can put back on the "Add bathroom" commit and check it out by one simple command: git rebase --continue
Actually even git itself is warning us if we do a
git status
that we are in an interactive rebase session, and we should abort or continue it.
All seem so simple now, but still...
Let's make the bathroom much bigger and with 2 windows, also add a pantry, and make a commit, named "Resize bathroom and add pantry":
- room_name: Bathroom,
size: 20m2
windows: 2
doors: 1
- room_name: Pantry,
size: 5m2
windows: 0
doors: 1
Now comes the problem. We decide to do a toilet not the bathroom on this level of the castle. So just to clarify, we decide to do it like this:
# This is my git castle!
---
location: Camelot
built: 2010-10-10
owner: Penny Bunn
rooms:
- room_name: Living Room
size: 30m2
windows: 3
doors: 2
- room_name: Kitchen
size: 15m2
windows: 2
doors: 1
- room_name: Toilet
size: 3m2
windows: 0
doors: 1
- room_name: Pantry,
size: 5m2
windows: 0
doors: 1
Ok, simple enough. We know interactive rebase, so we can do the whole swap at the "Add bathroom" commit. We simply go back to "Add bathroom" to make it "Add Toilet", also changing the yaml file. We do the rebase relative to the initial commit, edit "Add bathroom", and create toilet in the place of the bathroom:
- room_name: Toilet
size: 3m2
windows: 0
doors: 1
Do git add
and git commit --amend
as "Add Toilet" then simply git rebase --continue
.
But this time we get an error:
git-castle> git rebase --continue
Auto-merging castle.yaml
CONFLICT (content): Merge conflict in castle.yaml
error: could not apply 30637b3... Resize bathroom and add pantry
Resolve all conflicts manually, mark them as resolved with
"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".
Could not apply 30637b3... Resize bathroom and add pantry
Now, this happened because in the "Resize bathroom and add pantry" commit we also edited the same lines. All git sees is that we did an edit on the new "Add Toilet" commit on those lines, then while rebasing it realizes that there are conflicting edits on the same line. It makes sense, since how should git know which edit should persist right? That's why the rebasing stops at this point so we can edit the file by choosing which change to persist on the commit where the conflict occurred (this time it's the "Resize bathroom and add pantry" commit).
If you open the file you can actually see that git even inserted assisting separators to see what content it's having a dilemma about. We do the fix, and with git add
and git rebase --continue
the rebasing goes through.
Naturally and edit window will jump up for the fixed commit, which we could rename as "Add pantry", as that is it's true content after there is no bathroom resize in it.
Woila.
Check gitk to be sure.
git push
Profit.
Top comments (3)
The merge conflict can also be avoided by combining the bathroom changes with its creation and adding the pantry separately.
But some conflicts are unavoidable. Knowing what is changing is the challenging part of a merge conflict. I wish tools were better at showing the commit tree of both files not just the files. As it stands I open two gitk windows to follow along.
I'm just gonna plan on reading this as many times as I can today so that I might remember it in the future and stop making 'fixed the fix' commits
Thanks! :D I hope it could help!