Git: beyond the basics
The git version control system has become the standard for modern software development thanks to its decentralised workflow, simple branching, powerful history manipulation, and speedy performance.
Sadly, git is not known for being easy to use, and its commands can feel cryptic and potentially destructive:
Many developers don’t go beyond their usual “need to know” commands:
git checkout -b my-new-feature # create a branch git add . # add all changes git commit -m "Add super new feature" # commit git push # push to a new branch on the remote # Open pull request on the project
This is fine for basic scenarios, but misses out on a lot of git’s power. Here’s a collection of tips to go beyond “survival mode”.
Viewing project history
Git is a version control tool, so lets use it to dig around the history of a project and inspect some previous versions.
Check out an old version
With a project history like this:
$ git log --oneline 57e02a5 (HEAD -> master, origin/master, origin/HEAD) Speed improvements c44a241 Refactor calculator 0705d69 Get calculator working cd77563 Initial commit
We are on the
master branch at revision
To go back in time to the “Get calculator working” commit, pass the revision to
$ git checkout 0705d69 Note: checking out '0705d69'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b <new-branch-name> HEAD is now at 0705d69 Get calculator working
You could also use the shortcut
HEAD~2, essentially “2 commits before the current”.
There are lots of other ways to refer to commits quickly too.
Go back to the most recent commit again with
git checkout master.
Find out who made changes to a file
def main(): # call_webservice() fetch_data() process_data() save_results()
There’s nothing worse than a team-member commenting out code without explaining why.
Can we enable it again?
Find out with
$ git blame app.py dfdbde42 (Glynn Forrest 2018-06-29 13:42:12 +0100 1) def main(): e13c420a (Joe Bloggs 2019-01-07 09:55:39 +0100 2) # call_webservice() dfdbde42 (Glynn Forrest 2018-06-29 13:42:12 +0100 3) fetch_data() dfdbde42 (Glynn Forrest 2018-06-29 13:42:12 +0100 4) save_results() dfdbde42 (Glynn Forrest 2018-06-29 13:42:12 +0100 5) process_data()
Looks like commit
e13c420a will have some answers:
$ git show e13c420a commit e13c420aa990d3f711cfc4d971fbc44332e6e129 Author: Joe Bloggs <firstname.lastname@example.org> Date: Mon Jan 7 09:55:39 2019 +0000 Disable the webservice call for now. The API is down and we don't have proper error handling yet. Uncomment when its back up and we can handle the errors reliably. diff --git a/app.py b/app.py index a2ee6ed..f2fca1e 100644 --- a/app.py +++ b/app.py @@ -1,2 +1,2 @@ main(): - call_webservice() + # call_webservice()
Perfect, our co-worker wrote a good commit message, so there’s a helpful sentence explaining what happened.
As well as using
git blame on the command line, services like Github have line blaming built into their online code viewer.
Git lets you rewrite the history of a repository, with some caveats. Here are some common scenarios:
Made a spelling mistake in your most recent commit?
git commit -m 'Sped improvments' # whoops
No problem. Make sure there is nothing in the index with
git status, then run
git commit again with the
git commit --amend -m 'Speed improvements' # better
--amend will change the hash of your commit, and the version history is now considered different.
# Before 47eb21a (HEAD -> master, origin/master, origin/HEAD) Sped improvments c44a241 Refactor calculator # After 57e02a5 (HEAD -> master, origin/master, origin/HEAD) Speed improvements c44a241 Refactor calculator
47eb21a has been replaced with a new commit
If you’ve pushed the old commit to a remote, git will consider your local branch different from the remote, and not let you push without using
Adjusting the contents of commits
You can also use
--amend to add changes you forgot about.
57e02a5 (HEAD -> master, origin/master, origin/HEAD) Commit forgotten test file c44a241 Calculator can do scientific notation
you can adjust a commit you’ve previously made:
git add app.py git commit -m 'Calculator can do scientific notation' # whoops, forgot about the test file git add app_test.py git commit --amend
--amend is used without
-m, you’ll be prompted to use the existing commit message.
Beyond simple changes to the most recent commit, you’ll need to reach for the rebase command.
It is a powerful tool, making it possible to:
- Rework commits
- Change the commit order
- Combine multiple commits into one
- Remove commits from history
- Change the point in history a branch started (the parent commit)
This power comes with responsibilities - you could delete your work! Make sure to practise on a dummy repository, and check out this excellent post on Thoughbot’s blog to learn more.
Keeping pull request branches up to date
Pull requests can quickly get out of date while they’re being reviewed, especially in a busy project.
You’ll often be asked to keep your branch up to date with the main branch (e.g.
master) to show your changes are compatible with the latest version of the project, and to ensure there are no conflicts when your branch is merged.
There are two typical strategies: Merging the main branch into yours, and rebasing your branch on top of the main branch.
Merging the main branch
git checkout master # checkout main branch git pull # pull down the latest changes git checkout my-feature # checkout pull request branch git merge master # merge master into your branch, # the opposite direction of your pull request # you may need to resolve conflicts here git push # update your pull request
Rebasing onto the main branch
If you created the
my-feature branch after commit
B, you could rebase your branch to make it look like you started after commit
git checkout master # checkout main branch git pull # pull down the latest changes git checkout my-feature # checkout pull request branch git rebase master # rewrite my-feature to start from commit C git push --force # update your pull request
Before D---E my-feature / A---B---C master After D'--E' my-feature / A---B---C master
Note that all the commits on
my-feature will have different hashes.
You’ll need to push with
--force to overwrite history on your remote branch.
Git has a lot of (confusing) features, but, with a bit of perseverance, these features can save you significant time and effort. This article just scratches the surface, so check out these resources for more tips:
Good luck with your git learning, may all your merges be conflict free!