Git: Level Up
So you’re using git for your projects, you’ve got that whole committing thing down, you’re pushing and pulling like a sailor and branching like there’s no tomorrow.
Maybe you feel like you’ve got the basics down but you could do more? Here are some useful git features you should start using.
First, a general friendly reminder — don’t modify commits that have been pushed to the server! This can cause a big mess if others have pulled your branch already.
You might want to open a local test repository (just git init
in some folder) to try these commands out first. Some of them can erase your previous changes if you misuse them, so it’s better to get the hang of them away from your production code.
Amend: Change Your Last Commit
Made a commit and then remembered you forgot to add a file? Found a typo or decided to change your commit message? If it’s just a quick fix there’s no reason to clutter up the log with another commit, just stage (add) the new changes and amend the last commit. You can edit the commit message if you wish.
Here is the original commit:
commit e866a11345a2f3d5cb362f86e1db19deba9bbeb6
Author: Daphna Regev
Date: Sat Dec 23 14:14:33 2017 +0200added some feature
Let’s say we made some minor changes and want to amend our commit and change the message as well.
git add test.py
git commit --amend -m "added some feature and tests"
If you look at your log before and after you amend you’ll see that behind the scenes git is actually discarding your previous commit and creating a new one with a different hash. This means that you shouldn’t amend commits that have been pushed to the remote server.
commit 61b08b064e1dfaca2fc03ec4a2fe695f171857bf
Author: Daphna Regev
Date: Sat Dec 23 14:14:33 2017 +0200added some feature and tests
Cherry-Pick
Let’s say you branched out of your master branch to work on some feature, and so has Dana. While working on her feature, Dana finds and fixes some bug that affects your code as well. We want her fix in our code, but we can’t merge in her half-baked branch, and there’s no time to wait for her to finish the rest of her work. In this case we can cherry-pick a commit from one branch to another!
Each commit has a hash that identifies it. You can see it by looking at the log:
$ git log
commit 6edc562d07af572c481938f92acbc4e75f496564
Author: Daphna Regev
Date: Sat Dec 23 14:28:07 2017 +0200a bug fixcommit 61b08b064e1dfaca2fc03ec4a2fe695f171857bf
Author: Daphna Regev
Date: Sat Dec 23 14:14:33 2017 +0200added some feature and tests
When referencing a commit, it is usually enough to use the first 7 characters of the hash to uniquely identify it. We’ll need to switch to our target branch (for example master).
git checkout master
git cherry-pick 6edc562
Looking at the log, you’ll see that this is a new commit with a different hash, with the same content as the one we cherry-picked. Cherry-pick is like a merge, if it can’t be applied because of conflicts you’ll be asked to manually resolve them.
commit cbc1fd09086e2d4678860035db7cd9f4ac16f0a2
Author: Daphna Regev
Date: Sat Dec 23 14:28:07 2017 +0200a bug fixcommit 477ea14fbe9ddb2a542bdc59a6282424b8ac1667
Author: Daphna Regev
Date: Sat Dec 23 14:41:27 2017 +0200working on something elsecommit 61b08b064e1dfaca2fc03ec4a2fe695f171857bf
Author: Daphna Regev
Date: Sat Dec 23 14:14:33 2017 +0200added some feature and tests
Stash: Saving Changes
If you have work that’s uncommitted and you need to checkout another branch, you’ll get this error message from git:
$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
test.py
Please, commit your changes or stash them before you can switch branches.
Aborting
Of course you can commit your unfinished work, switch branches, and return later to finish it (you could use amend!). But sometimes you just want your changes out of the way for a moment, for example you might want to remind yourself how something was working before you made a mess of it. For that we can use git stash.
Git stash takes all your modifications to staged and tracked files, and adds them to a stack of unfinished changes, after which your working directory will be clean. Kind of like pushing all of your mess into the closet when your mom tells you to clean up your room.
$ git stash
Saved working directory and index state WIP on my_feature_branch: 6edc562 a bug fix
HEAD is now at 6edc562 a bug fix$ git status
On branch my_feature_branch
nothing to commit, working directory clean
You can then switch branches freely, and when you want to reapply your changes (to any branch, not just the one you were working on originally) use git stash apply.
$ git stash apply
On branch my_feature_branch
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)modified: test.pyno changes added to commit (use "git add" and/or "git commit -a")
Interactive Rebase
This one is a bit more tricky, but quite simple once you get the hang of it. On one hand, when we use git we want to make small commits often. It makes merging branches easier, it enables cherry-picking only the changes relevant to us and it’s easier to revert changes if needed. On the other hand, we’d like to keep our history tidy so we can understand what’s going on and not see this:
commit 09c9cd318a87d64b507de1b53603a48501cadb8d
Author: Daphna Regev
Date: Sat Dec 23 15:17:43 2017 +0200oops forgot to add something to first functionalitycommit 995930513912f7cc2e75a059eac8b3d4053a41f4
Author: Daphna Regev
Date: Sat Dec 23 15:17:05 2017 +0200move this functioncommit fe2b599f44d4a3d41aa2914388580408a77f5319
Author: Daphna Regev
Date: Sat Dec 23 15:16:22 2017 +0200add another functionalitycommit 7465037107cb7c55c8b3200c802d8decb3e9d82c
Author: Daphna Regev
Date: Sat Dec 23 15:15:24 2017 +0200fixed testscommit 04525a3996bb31fac1335c446d99118070c62c99
Author: Daphna Regev
Date: Sat Dec 23 15:14:36 2017 +0200add some functionality
Remember that the log shows us commits from newest to oldest.
How do we do this? We continue to make lots of small commits throughout our work, and when we’re done we use git rebase -i HEAD~x, x being the number of commits we want to review going back from HEAD (which is just a pointer to our last commit in the current branch).
git rebase -i HEAD~5
Now the editor opens with a list of these last commits. Notice that the order is oldest to newest, as opposed to the order we see in git log. Git helpfully gives us a list of the commands we can use (it usually does nice things for us like that).
pick 04525a3 add some functionality
pick 7465037 fixed tests
pick fe2b599 add another functionality
pick 9959305 move this function
pick 09c9cd3 oops forgot to add something to first functionality# Rebase b28fddc..09c9cd3 onto b28fddc
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
What do we do now? First off, we can change the order of the commits by cutting and pasting lines to new positions. We can edit the log message by changing “pick” before the commit to reword (or -r). But more importantly, we can squash commits together if they belong to the same concept. This will result in a single commit after the rebase is done, with a commit message containing all the messages of the squashed commits, though we can edit it to whatever we like. Just change “pick” to squash (or -s). Alternatively we can use squash’s little brother fixup (or -f), which squashes without keeping the commit message. This is useful for small updates, maybe additional tests or typo fixes we didn’t amend earlier.
pick 04525a3 add some functionality
fixup 09c9cd3 oops forgot to add something to first functionality
squash 7465037 fixed tests
pick fe2b599 add another functionality
squash 9959305 move this function
Once we’ve closed the rebase editor, git will replay the commits by their new order. If there is a conflict we’ll be asked to resolve it the regular way before proceeding. If we squashed any commits, we’ll get the editor again in case we want to change the commit message. This actually creates new commits so again, no messing with commits that have been pushed to remote.
commit dfc9fd3459a85af879910ffc5bec7ecfcc48091b
Author: Daphna Regev
Date: Sat Dec 23 15:16:22 2017 +0200add another functionality
move this functioncommit eacf4b2c103693c8fc15006586390bff5041fcfb
Author: Daphna Regev
Date: Sat Dec 23 15:14:36 2017 +0200add some functionality
fixed tests
Git is a powerful tool and knowing some advanced commands can help you be more productive and organized. I gave simple examples on purpose, each of these commands has more options and use cases that I encourage you to explore. I hope you find these commands useful, and start using them in your day to day work!