One of the greatest features of the GIT version control system is its ability to rewrite local history before pushing it to a remote repository.
Yes. Even you, a lowly programmer with your forgetfulness, clumsiness and caffeine shakes can make yourself look like a finely tuned programming God.
This allows you to go back in time and start your work again with all the knowledge and code you have in the present; to reword and reorder the changes.
Git allows you to go back in time and “undo” your mistakes. Yes. Even you, a lowly programmer with your forgetfulness, clumsiness and caffeine shakes can make yourself look like a finely tuned programming God. Make it look like you never commit a mistake, never break a test.
Read on and I will show you how.
DISCLAIMER: Rewriting Git history shouldn’t actually be about your ego or posing as a perfect programming God. It’s actually about making precise, succinct changes with the minimal fuss and good descriptions so that your colleagues can easily understand and review your changes.
Before We Even Start
Almost everything in this article builds on a single core assumption: you are creating good commits to begin with. You are not writing a Russian novel. You are not writing a personal diary of your hourly mood swings. You are not venting your occupational frustrations 72 characters at at a time. You are describing your changes as clearly as you possibly can for your colleagues and your future self.
First. Commits should be well written and descriptive of their content. A good guide to commit message guidelines can be found here.
Second. Commits should never contain unrelated changes. This is very important (you’ll see why later). Ideally a commit should include changes to a single file plus perhaps that file’s unit test. If additional changes in related files must be made, it’s best to keep those changes to the “bare minimum to make the tests pass”.
Third. Keep commits relatively small. There is no golden rule here, but adding a single function (plus related unit test) is about the size you want to aim for.
One of the greatest features of the GIT version control system is its ability to rewrite local history before pushing it to a remote repository.
The “Rebase Over Master”
This is the bread and butter of all the git manoeuvres. Any Git Citizen worth their hashes should be doing this multiple times a day. This ensures the latest code from the master (or mainline) branch is always incorporated before your changes even begin to happen. Settling the issues and conflicts up-front may require a little more work, but avoids a number of pitfalls common when teams work in the same code.
Remember! You can’t trust passing tests until you’ve incorporated all the code in master into your branch.
When to Use It
- Your colleague has just merged work into master and you want to get up-to-date with the latest code.
- Immediately before creating a Pull Request.
- Immediately before merging a Pull Request.
How to Do It
git pull origin master # Update master with latest code
git rebase master # Rebase over master non-interactively to solve merge conflicts
The Interactive Rebase Over Master” - A.K.A. the “tidy up”
This technique essentially rolls back time to when you first branched off the master (or mainline) branch and allows you to step through your work commit by commit making small tweaks as you go.
When to Use It
- Immediately before creating a Pull Request, when you want to tidy and/or reorder your commits so that they make more sense.
How to Do It
git pull origin master # Update master with latest code
git rebase master # Rebase over master non-interactively to solve merge conflicts
git rebase -i master # Rebase again interactively to clean up history
You should now see a log open up in your favourite editor. Each commit is listed in order with the word PICK in uppercase preceding the hash and commit message. You can move the lines of this file into any order you want, and change the word PICK into any operation (listed below) which you would like to perform at this step in the history.
Hints
- Do a normal “rebase over master” first to make things simpler.
- If things get bad. Abort. The
git rebase --abort
command aborts the rebase and puts you back to where you began.
Sub-Manoeuvres
The “PICK” commit This commit stays in place as it is. Most commits will probably be left as “PICK”.
The “REORDER” By removing a commit from one line and moving it somewhere else, you can group related commits together in history. WARNING! This can create significant merge conflicts with yourself if you’re not careful. Only ever cross commits if you’re sure they are not touching the same files. You never feel quite as inadequate as when you get conflicts with yourself.
The “FIX” commit This commit is swallowed up into the previous commit. This is especially useful when combined with the “REORDER”. This allows you to move fixes and tidies immediately after the commit which they are trying to fix. By marking them as “FIX” the original commit is updated and the issue essentially “disappears”.
The “SQUASH” commit This is similar to the “FIX” commit, but is useful when you want to retain the commit messages of both. This squashes the messages together.
The “RENAME” commit This simply updates the commit message. This is useful for adding details to a commit message or fixing typos.
The “DELETE” This removes the commit from the branch. WARNING! Unless this commit has been cherry picked into another branch (see the split branch manoeuvre) or you have created a second branch with this commit present, this may result in you losing the changes present in this commit. So be careful. HINT: You don’t actually have to update the operation, you can just delete the whole line.
The others These other operations aren’t commonly useful. Feel free to read about their uses elsewhere if you’re curious.
Amend - A.K.A. “just one more thing”
The quick and dirty technique for tacking on a tiny change to the previous one. This is very similar to the “FIX” operation during an interactive rebase.
When to Use
- Small arbitrary fixes related to the previous commit.
- Fixing tests that were broken with the previous commit.
- Fixing linting issues that were introduced by the previous commit.
How to Do It
git add ./src/my-tiny-fix
git commit --amend
Cherry Pick
Elegant and precise, the cherry pick pulls one or more commits from one branch and reapplies them in another.
When to Use
You added something useful in a branch that could be useful in other branches immediately. You fixed something in your branch and want to merge the fix immediately without the rest of your work.
How to Do It
git checkout -b new-branch-name # Create a new branch
git cherry-pick f0b24bcd023cd2d2 # Pick the specific commit hash from the old branch
The “Soft Reset” - A.K.A. the “start again”
When to Use It
- Your branch has become complicated and unwieldy. Your commits have contradicted each other. Changes have been made, undone and redone. You’re lost in a swirling vortex of your own making. Your options are rocking back and forth in the foetal position or starting again.
How to Do It
git log --no-merges --stat --reverse master.. # Display all commits made since your branched
git reset --soft f0b24bcd023cd2d2 # Reset to the last good commit
You now should have un-staged changes ready to be re-added and re-committed piece by piece. Do it better this time you sorry excuse for a sentient being.
The “Split Branch” - A.K.A. “my PR is too big”
The idea behind this manoeuvre is we create 2 identical branches, then we use the “interactive rebase” manoeuvre (see above) in each branch to remove opposite pieces until we get 2 entirely distinct branches.
When to Use It
- You’ve been overzealous and implemented 2 features in the same branch. Your colleagues have shamed you into splitting it into separate PRs. You need to begrudgingly oblige.
- You’ve foolishly performed a major refactor and implemented an unrelated feature in the same branch. You want to submit your feature for code review while you ponder just how in the hell your refactor broke so many god damn tests.
How to Do It
git checkout -b my-new-feature-branch # create a branch identical to your current one
git rebase -i master # Interactive rebase.
# Open log in editor. Remove all commits relating to feature A
git checkout my-old-feature-branch # checkout the previous branch
git rebase -i master # Interactive rebase
# Open log in editor. Remove all commits relating to feature B
there is nothing that Git stash gives you, that a WIP commit does not, and then some
The WIP commit and the Soft Reset - A.K.A. “I’ll pick this up later”
Forget Git stash. I’ve heard it argued that “there is nothing that Git stash gives you, that a WIP commit does not, and then some”. Over time I’ve come to agree.
You may be familiar with the git stash command. It’s when you want to stash your work for later but are in too much of a rush to give it a name or distinguish between “real” changes and the bullshit console.logs and debug statements with which you have undoubtedly sullied your workspace with.
Stash comes with a few caveats. The most notable is that git stashes are not easily shared remotely. Another issue is that stashes are not intimately connected with the branches they originated from. It’s very easy, for instance, to switch back to an old branch from which you have stashed changes and simply forget to reapply the stash.
Choirs sing, the heavens alight. Enter stage left: the “Work In Progress commit”.
Need to quickly switch branches but have some stuff on-the-go? No problem. Simply create a commit on your current branch with the message “WIP”. Push it up to remote (as long as it’s a feature branch!) so if you call in sick tomorrow you can still pick up where you left off. Or even better, you can tell a colleague to pick up where you left off leaving you free to binge on codeine and old episodes of Friends.
When you come back simply use our old friend the “soft reset” to delete the WIP commit but keep all the changes.
You can thank me later.
When To Use It
- To replace git stash
Hot To Do It
git commit -am "WIP" # create your WIP commit
git checkout my-other-branch # switch to your branch or do whatever you need to
git checkout my-previous-branch # go back to your old branch
git reset --soft HEAD~1 # soft reset backwards 1 commit
The Uh-Oh - A.K.A. the “oh shit I lost my work”
At some point in your career you will screw up. 99% of all software screw-ups are git related. Fact.
Instead of panicking, take a deep breath and realise git has got your back.
Introducing git reflog
, your git safety net. When excrement hits the
proverbial fan, this is the command that is going to save your bacon.
When To Use It
- When you’re holding your head in disgrace contemplating the sheer vastness of your git inadequacies.
How To Do It
git reflog # Open the reflog
# pinpoint the commit you've lost or want to return to
git checkout b3a343ff0a # Checkout the commit
git checkout -b my-branch-i-though-lost # Create a new branch pointing to the current commit
You should be presented with a long list of your recent git buffoonery. Every checkout, merge, and rebase should be noted with a description and a hash. Even if you accidentally deleted your branch, chances are those files are still hanging around until git cleans itself (which it does every so often).
Simply find the commit hash and check it out, then create a new branch for it.
All is well in the world. Have a beer. Remember to keep being a great git citizen. :)