Backbeat Software
Photo by Robbin Huang on Unsplash

Git: beyond the basics

Some tips to take your git knowledge beyond beginner level.

Glynn Forrest
Thursday, January 31, 2019

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:

xkcd: Git

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 57e02a5. To go back in time to the “Get calculator working” commit, pass the revision to git checkout:

$ 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:

$ 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 <joe@company.com>
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.

Rewriting history

Git lets you rewrite the history of a repository, with some caveats. Here are some common scenarios:

Rewording commits

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 --amend option:

git commit --amend -m 'Speed improvements' # better

IMPORTANT: --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

The commit 47eb21a has been replaced with a new commit 57e02a5. 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 -force.

Adjusting the contents of commits

You can also use --amend to add changes you forgot about. Instead of:

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

If --amend is used without -m, you’ll be prompted to use the existing commit message.

Rebasing

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 C:

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.

Further reading

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!

More from the blog

The editor hotkeys hiding in plain sight cover image

The editor hotkeys hiding in plain sight

The vim and emacs hotkeys embedded in your everyday applications.


Glynn Forrest
Saturday, November 30, 2019

Sending emails with Symfony: Swift Mailer or the Mailer component? cover image

Sending emails with Symfony: Swift Mailer or the Mailer component?

The evolution of Symfony’s email tooling, and what to use for a brand new application.


Glynn Forrest
Thursday, October 31, 2019

Why Symfony's container is fast cover image

Why Symfony's container is fast

Why Symfony’s dependency injection container is both full of features and lightning fast.


Glynn Forrest
Monday, September 30, 2019