Advanced Git Commands You Will Actually Use

Title says it all, this post will introduce you to useful git commands you may not already know

 ·  11 minutes read

This is the blog version of the talk I gave at Samsung's UK office. People have been asking for a blog version, so here it is. While it covers mostly the same topic as the talk, it's not exactly the same. If you are only going to look at one of them, it's better to read this post.

I'm intentionally brief throughout this post. The goal of this post is to expose you to new commands you may have not known. However, it should only serve as a starting point, and it's up to you to further explore those you found useful.

This post assumes some basic working knowledge of git, though still covers some basics. If you feel like you're already familiar with git, you can skip past the introduction

Quick Setup Before We Begin

In case you don't already have these set, you should set your name and email. I set the globally to all repos, but if you omit the --global-- directive, you can set them locally per repo.

$ git config --global user.name "Tom Hacohen"
$ git config --global user.email "tom.git@stosb.com"

You should also enable colours, set your default editor (what git will use for editing commit messages, for example) and an external tool that can be used to view diffs when running git difftool

$ git config --global color.ui true # Default since git 1.8.4
$ export EDITOR="vim" # Optional
$ git config --global diff.tool "vimdiff" # Extra optional

You should also enable git command autocompletion (system dependant).

At this point it's also worth mentioning that git has built-in support for aliases. Some of the commands presented here will be rather long, so it may be useful to alias some of the more commonly used ones.

$ git config --global alias.unstage 'reset HEAD --' # Set it up
$ git unstage # Use it

A Few Useful Properties of Git

Branches and Tags are References

Commit graph

Branches and tags are just references to commits, and master is just a normal branch, so a reference too. This means you can make branches and tags point to any commit in your history, or use them like you would any commit hash.

Nearly Every Operation is Local

Because of git's decentralised nature, all the operations other than the ones that sync with a remote (either push or pull/fetch) are local. This means that:

  • Operations are usually very fast
  • You can wore offline, while for example on a plane
  • You can be sloppy and clean up after yourself before pushing your changes.

A Few More Useful Properties…

  • Has a staging area, so you can add only some of your changes and commit when ready.
  • You can rewrite history by rearranging and amending commits.
  • History is immutable, so all of your commits are recoverable, even if you rewrote history (unless you ran garbage collection).
  • Everything is referenced by its cryptographic hash, so you can be certain your commit history or files have not bee corrupted or maliciously modified.

Referencing Commits

As mentioned above, branches, tags and commit hashes can be used interchangeably. There is also a special pointer called HEAD that always points to the current commit/state we are on.

In addition, git supports a few modifiers that make referencing your commits very easy.

$ git show 7c1c793fe8769980823bfbfcf80396486c6163d7 # Full commit hash
$ git show 7c1c793fe87 # Shorter hash
$ git show 7c1c793 # Even shorter hash (can be any length as long as not ambiguous)

$ git show HEAD # Current head
$ git show master # The commit master points to
$ git show some_tag # The commit some_tag points to

## Reminder: all reference types can be used interchangeably
$ git show HEAD^ # HEAD's parent
$ git show master^^ # master's grandparent
$ git show some_tag~3 # some_tags's great-grandparent
$ git show 7c1c793~4 # This commit's great-great-grandparent

$ git show master@{yesterday} # The commit master pointed to yesterday

Reading the Log

Cleaner Tree Log Overview

Listing a Change Summary

Seeing Actual Code Changes

Limiting Commits

## Commits changing file/function in file
$ git log main.c
$ git log -L :list_find:main.c

## Commits containing string
$ git log --grep FOOBAR # Messages containing string
$ git log -S FOOBAR # Lines containing string

## Commits in HEAD, not in master
$ git log master..

## Commits that are in either "foo" or "bar" (not both)
$ git log foo...bar

## Only show the first parent (don't show commits in merged branches)
$ git log --first-parent

Finding Out What You Have Been Up To

## The total number of commits by an author
$ git shortlog -nse --author=tom.git@stosb.com

## The total number of commits by an author in the last year
$ git shortlog -nse --author=tom --since="1 year ago"

## A list of commits by an author in the last year
$ git log --author=tom --since="1 year ago"

Getting a Version Description

While git's hashes are useful for pointing to specific commits, they are not very convenient as version identifiers as it's not possible to know which commit is newer just by looking at it.

There are two ways to go around this. The first is using git decsribe, a built in git command that prints out a usable description for a commit. For example, the commit below was 236 commits after the tag v1.17.0 and its short hash was gc66d478. Or alternatively, it was 12514 commits since the origin of the repo.

## Get a standard version description (requires at least one tag)
$ git describe --long
v1.17.0-236-gc66d478

## Get the SVN-like monotonic revision number
$ git rev-list --count HEAD
12514

Inspecting Commits and State

## Showing the changes in a commit
$ git show 80f14e8fea0057ee950f0778dd51b096ca9850a4
$ git show my_branch # Can be branch, tag or whatever.

## Showing a file from a different state
$ git show v1.7.0:main.c

## Switching working directory to a different reference
$ git checkout c9b306777 # Or any other reference

Branches

Viewing

## All the branches (including remote)
$ git branch -a
## Use "git fetch -p" to clean up stale remote branches

## All branches that are fully contained in HEAD
$ git branch -a --merged

## All branches that are not full contained in HEAD
$ git branch -a --no-merged

Manipulating

I think it's very important to maintain a linear history. Most of the commands here help you maintain that.

Rebase reorders your current branch over another, and git merge --no-ff makes sure that merges always create a merge commit, even if not necessary (which it never is when you have linear history). This helps you group changes together like you would with any other merge.

Last, but not least is the --preserve flag which makes sure the aforementioned redundant merge commits are not discarded when rebasing.

## Rebase branch over the upstream version
$ git pull --rebase # Can be set in config

## Rebase branch over a specific branch
$ git rebase origin/master

## Merge a branch and always create a merge commit
$ git merge --no-ff

## Rebase and keep the branch structure
$ git pull --rebase=preserve
$ git rebase --preserve-merges origin/master

## Applying a commit from a different branch
$ git cherry-pick 80f122437d

Making Changes

Inspecting Workspace State

## A more condensed status
$ git status -s

## Changes compare to upsteram
$ git diff origin/master

## Seeing the diff of the staging area
$ git diff --cached

## Ignore whitespace changes in diff
$ git diff -w

Adding Files to the Staging Area

## Adding parts of a file
$ git add -p file # File can also be a dir, or ommitted

## Adding all of the changed files in a directory
## Very useful when resolving conflicts
$ git add -u src/

Using the Stash

## Stashing all of the changes
$ git stash

## Stashing some of the changes
$ git stash -p

## Applying back the stash
$ git stash apply

## Stash has many more features I do not use
$ git stash --help

Rewriting History

It is a very bad idea to rewrite published history. That is, history that has been shared with the world (by for example, git push) to a shared branch. It's OK to change the history of a temporary feature branch, and is for example how you update a PR on GitHub and GitLab.

This section is therefore here to help you rewriting your local history before you publish, so the published commit history is clean and easy to follow.

Un-staging Files

Editing the Most Recent Commits

## Remove the most recent commits and their changes
$ git reset --hard HEAD^
$ git reset --hard HEAD~3 # Or any other pointer (for a range)
$ git reset --hard origin/master # Reset the state to upsteam

## Keep the changes uncommitted
$ git reset HEAD^
$ git reset c9b306777 # Or any other pointer

## Unstage changes
$ get reset

## Merging index into the most recent commit
$ git add NEWS
$ git commit --amend # Also lets you edit the commit message
## Add -v to git commit to also see the diff

## Edit the author
$ git commit --author "007 <jb@mi6.gov.uk>" --amend

The Most Useful Command in The World

This is hands down the most useful command. I use it a lot! This command lets you edit all of the commits in a certain range, and I use it to rearrange and clean up my commits.

Recovering Lost Commits

The problem with changing history is that history can be lost. It's very common to want to revert to a previous state or recover an accidentally lost or modified commit.

Since git's history is immutable, commits are never lost (unless garbage collected and can still be referenced by their hash, or if the hash is unknown, can be found using the method below.

Afterwards you can just cherry-pick, reset, checkout or whatever method to recover those commits.

Removing Parts of a Commit

It's also very common to accidentally commit a file or a line that you didn't intend to. Especially when using git commit -a. This too can be easily fixed.

## Commit c42bc3a535 (can be anywhere in history)
$ git revert -n c42bc3a535
$ git reset # Remove everything from staging

## Add back the wanted changes
$ git add NEWS # All of this file
$ git add -p # Some parts of the rest

## Merge the commit into the original commit
## Either amend if it is the HEAD
$ git commit --amend
$ git checkout -f # Remove the rest of the changes
## Or fixup if anywhere else
$ git commit -m "Temp"
$ git checkout -f # Remove the rest of the changes
$ git rebase -i c42bc3a535^ # Mind the ^ (caret)

Delivering Changes

## Change the url of the repository
$ git remote set-url origin ssh://git@newserver.com/repo.git

## Adding a new remote
$ git remote add new ssh://git@alt.newserver.com/repo.git

## Using the new remote
$ git fetch new
$ git rebase new/master
$ git push new master

## Generate patch files for a series of commits
$ git format-patch HEAD~5 # Or any other reference

Investigating Bugs

Finding Who Added a Line and Why

## Check who changed the file
$ git blame eo.c # Add "-w" to ignore white-space

06f65ab2 eo.c (Tom Hacohen       2016-05-19 11:33:17 +0100
    321)         vtable = &amp;klass-&gt;vtable;
fc880379 eo.c (Tom Hacohen       2015-11-09 11:45:04 +0000
    322)    inputklass = main_klass = klass;
7be0748b eo.c (Jérémy Zurcher    2013-07-30 15:02:35 +0200
    323) 
c2b4137f eo.c (Carsten Haitzler  2015-10-24 12:23:53 +0900
    324)    if (!cache-&gt;op)

## At an earlier revision
$ git blame fc880379^ -- eo.c

Finding When a Bug Was Introduced

This command helps you run a binary search on the commit history efficiently finding a bad commit.

All you need to do is run bisect, evaluate a commit to see if it was broken or not, and then report the result to git. You can even skip commits by passing skip instead of good or bad.

$ git bisect start
## To limit bisect to a directory: "git bisect start -- src/"
## Set the initial known good and bad commits
$ git bisect bad COMMIT
$ git bisect good COMMIT
Bisecting: 417 revisions left to test after this (roughly 9 steps)
[7352bcff98fc65a08edcd505b872403af8d821a7] edje_external: fix external icon handling

$ git bisect good  # Or bad if bad
Bisecting: 208 revisions left to test after this (roughly 8 steps)
[9f5d27972252d67fe92ca44a1c610da4ed531b86] Evas events: Implement support for hold event

## ... SNIP ...

a31f399857ecf9409e6aa6fb8effe9477ee47fe2 is the first bad commit

Automatic Bisect

While git bisect saves you a lot of time and effort, it still involves manually testing commits and reporting the results back to git. You can alternatively write a script that will automatically evaluate commits for you.

For example consider this script:

#!/bin/sh
make || exit 125                     # this skips broken builds
~/check_issue.sh                     # does the test case pass?

It compiles the code, and runs ~/check_issue.sh, our small test program that returns 0 on success, and anything else on failure. Then all you need to do is run:

$ git bisect start HEAD HEAD~10 --   # Last 10 commits, short-hand form for start
$ git bisect run ~/test.sh
$ git bisect reset                   # quit the bisect session

a31f399857ecf9409e6aa6fb8effe9477ee47fe2 is the first bad commit

Releasing Versions

We have now finally fixed all the bugs, merged all the feature branches, cleaned up the change history, and pushed our changes to our remote repository. There is only one thing left, tagging the release.

## Bare tag, just hold a reference to a commit: not recommended.
$ git tag v1.0.0 # You can also pass an optional commit reference

## Annotated tag, also add a message to the commit, for example, a changelog: recommended
$ git tag -a v1.0.0 # Again, you can also pass an optional commit reference

## Cryptographically igned tags: assuring your users this tag really came from you: recommended
$ git tag -s v1.0.0 # Again, you can also pass an optional commit reference

A Few More Commands Worth Checking Out

  • git submodule
  • git send-email
  • git checkout -b
  • Use git with other VCS: git-svn, git-hg and more
  • CLI viewer: tig
  • GUI viewers: gitg and gitk

Finishing Notes

Git is a powerful tool, and this post only scratches the tip of the iceberg. I highly recommend you take the time to read the wonderful git book. It's not a very long read, and is well worth it.

Please let me know if you spotted any mistakes or have any suggestions, and follow me on Twitter or RSS for updates.

git programming