Telling your story
Overview
Teaching: 30 min
Exercises: 0 minQuestions
How can we change the apparent git history to make it clearer
Objectives
Learn to change the way history appears
Episode setup
First we need to switch to some code
$ cd ~/git-demystified/episode_7.1
Lets look at the history of the current branch.
$ git log --oneline --all --graph
Reverting changes
We see a commit “Phantom commit” in our history. Remember that this adds the file phantom_file. Let us remove this file, by undoing all the changes in this commit.
$ cd ~/git-demystified/episode_7.1
Check the file is there
$ ls
And undo the change
$ git revert e93c7
check the log
$ git log --oneline --all --graph
Show the latest commit
$ git show HEAD
Notice that git revert
creates a new commit. This is done
intentionally, so that we don’t have to change any of the commits
already pushed to the remote (which other people may have access to).
Pushing
Changing commits which other people have access to is a bad idea, because they won’t be able to build on those commits. Never change history further back than where you have pushed. You can normally see this with the label
origin/current-branch-name
(e.g.origin/master
) in your git log.
Rebasing
In git we have two options on how to integrate changes: merging and rebasing. Merges we’ve already encountered, and is by far the most common option. Always default to a merge if in doubt, but rebase gives you additional options with how to shape your git history.
Let us redo the previous merge as a rebase.
$ cd ~/git-demystified/episode_7.2
$ git checkout another-phantom-tracker
And try a rebase instead of merge this time. As before, we have a merge conflict that we have to resolve.
$ git rebase phantom-tracker
$ git status
Edit the merged file, choosing the lower option and a Goblin
.
$ nano phantom_file
add the merged file
$ git add phantom_file
continue the rebase
$ git rebase --continue
Have a look at the log
$ git log --oneline --all --graph
Look at the file
$ cat phantom_file
Beware the rebase
Never rebase any commits which anyone else may have based any work off (i.e. commits that you have pushed)
This is because rebase changes commit IDs, creating a parallel history of commits which contain similar content but are in fact not identically the same. This can cause problems to others.
If you follow this simple rule, and only rebase your own personal branches, then you can rebase as much as you like.
Amending commits
Let’s say we would like to make some changes to the last commit, let’s say we want to add a new file and change the wording of the commit. We create a new file
$ touch newfile.txt
We add it
$ git add newfile.txt
and we commit it with --amend
and a new message
$ git commit --amend -m 'This commit message has been changed'
and we verify we’ve made a change with
$ git log --oneline --all --graph
This is an easy way to make changes immediately after we’ve made a commit, and is perfectly safe provided we have not pushed the changes yet. It’s a great way of saving some “Oops” moments!
Interactive rebasing
There is one more use of rebasing, the interactive rebase. This allows us to make arbitrary changes to our history before we push, to have commits appear exactly as we’d like them. Let’s change the last 5 commits interactively
$ git checkout master
$ git rebase -i HEAD~5
Let’s make AUTHORS
appear as if we’d done it last, but moving that
to the end of the list. And we save the file. Let’s see our log
$ git log --oneline -10
We can see that the AUTHORS
commit now appears to have happened
last, despite the fact that in reality this is not the case.
Let’s say that now we think we want to merge the merges which change
README.mdown
text into one commit.
$ git rebase -i HEAD~5
We edit the file to change the action on the last but one entry to be squash rather than pick. When prompted, we set the message to “Change wording in README.mdown”. Let’s look at the log again
$ git log --oneline -10
Let’s now say we want to change commit message. We’ve spotted “URLS”, and we prefer “URLs”. We run the rebase command again
$ git rebase -i HEAD~5
And change the action of the commit with URLS to reword. This pops up a message to reword the commit. We can check that the rewording was a success with
$ git log --oneline -10
Remember: we’re changing commits
Remember that every time we rebase, and change anything in any way, we’re changing commit IDs. Never every do this to code that has been pushed or shared elsewhere.
A friendly warning
Rebasing seems like a very powerful tool, but is best when used sparingly. Extensive rebases which change the order of commits, and cause many conflicts, can easily be prone to human error and can cause some of the intermediate commits in history to be in inconsistent states without the developer noticing. It’s often best to avoid heavy rewrites of history, and reordering, and restrict rebasing to rewording and minor squashing commits where possible.
Pull with rebase
One of the most useful uses of rebase is if we have changes on our local version of master, and we would rather have a linear history rather than merges. In this case, when we pull, we can do a pull with a rebase. Let’s take a look at this
Pull with merge
$ cd ~/git-demystified/episode_7.3
First, we’ll do this as a merge
$ git fetch --all
Then we’ll do a normal git pull
$ git pull
And take a look
$ git log --oneline --all --graph
We see that git has create a merge commit for us, merging the remote
and local versions of master
. But, for a single commit, wouldn’t it
be nicer if the commit just appeared after all the work that has been
done on master
? Let’s reset our repository before merge and try
again. We’ll use the reflog to do that with
Pull with rebase
$ cd ~/git-demystified/episode_7.3
First, we’ll do this as a merge
$ git fetch --all
Then we’ll do a normal git pull
$ git pull
And take a look
$ git log --oneline --all --graph
Rather than a merge commit, git has replayed our local changes on top of the current state of master, it looks as if all the changes happened in a linear history.
It’s safe to
pull --rebase
Pulling with rebase doesn’t have many of the drawbacks of a normal rebase. Since our local changes are the ones which get rewritten, we know that with
git pull --rebase
that we won’t cause problems to other people as we’re re-writing history.
The good the bad and the ugly: rebase vs merge
There are two schools of though on the utility of rebasing.
On the one hand, you could view the commit history is a record of what actually happened. Changing it invalidates the validity of this historical record.
The other point of view is that the history should be a clear story, but not necessarily a historically accurate one, which describes in a useful way how the software came to be.
There is no right or wrong answer, but if we wish to do so, we can use rebasing to clean up own own story before we push to the world, as long as we never rebase anything we’ve already shared.
Key Points
Learnt to use git to revert changes and modify the last commit
Learnt to change history in any way that we like to clean up our development story
Learnt to continue from the last commit in master with
git pull --rebase