Jakub Arnold's Blog


Git Command Overview with Useful Flags and Aliases

This post is a short guide to making your git usage a little more efficient. We’re not going to cover how git works in depth. Instead, we’ll look at the most common operations and useful flags, with the goal to create a set of bash or zsh aliases for daily use. A completel list of aliases presented in the article is summarized at the end of the article.

Each section will first briefly describe the command, some of its useful flags, and then suggest a set of mnemonic aliases with their usage. Contrary to what some people might thinks, we won’t use the builtin git alias functionality using git config (that is e.g. git config --global alias.co checkout), but rather plain shell aliases such as alias gco="git checkout". The reason is simple, it is much easier and faster to type gco than git co, which makes git usage more enjoyable. Since (almost) all of our aliases will be prefixed with g (such as ga, gco, gc, …) they will be just as easy to discover if you ever forget them as their git config alias counterpart.

git status

We’re going to skip ahead alphabetically and cover git status right now, as it will be useful in explaining the other commands. Everyone who ever tried git had to write git status at some point, yet of all the people I’ve met, only a few know of its extremely useful variant git status -sb. Let me illustrate on a straightforward example where we simply create three file f1, f2, f3, modify some of them, and look at how the output of git status differs from git status -sb (I tried to make the example self-contained so you can try it yourself):

$ mkdir status-demo
$ cd status-demo
$ git init
Initialized empty Git repository in /home/darth/projects/status-demo/.git/
$ # This lets us make an empty commit
$ git commit -m "Initial commit" --allow-empty
[master (root-commit) 35773eb] Initial commit
$ touch f1 f2 f3
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        f1
        f2
        f3

nothing added to commit but untracked files present (use "git add" to track)
$ git status -sb
## master
?? f1
?? f2
?? f3
$ git add .
$ git commit -m "Add a few files"
[master b97a26a] Add a few files
 3 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 f1
 create mode 100644 f2
 create mode 100644 f3
$ echo x > f1
$ rm f2
$ echo x > f3
$ echo x > f4 # new file
$ git add f1
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   f1

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        deleted:    f2
        modified:   f3

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        f4

$ git status -sb
## master
M  f1
 D f2
 M f3
?? f4

The code examples are intentionally a bit verbose to keep them reproducible. You can simply follow along command by command in your own terminal. Playing around with git is a great way to learn!

Since the code highlighter on this post does not capture the terminal output highlighting, here’s the last two commands in a screenshot, showing how git status and git status -sb share the same color highlighting, but only format the output differently.

git status vs git status -sb

git status -sb simply drops all of the text and additional information, and keeps the important parts of the output - what branch are we on and what has changed in what way. It will mark modifications with M, deletions with D, and newly added files with ??. The column in which the M/D is displayed also signifies if the change was staged. For example, the modifications to f1 were staged with git add, hence the M displays in the left column and in yellow (same as regular git status). Modifications to f3 and the deletion of f2 were not staged, which is why they’re in green in the second column.

While this might be a little confusing at first, I promise that it only takes a couple of minutes to get used to this, and that your git experience will be much improved from using git status -sb over the regular git status. Suddenly your terminal won’t fill half the screen with each status, and you won’t have to scroll around to find your previous commands after using git status for a few times. It might seem like a small thing, but at least personally for me, this single command (along with the suggested alias) transformed my git usage from annoying to joyful.

To make the matters even more controversial, I suggest a different alias for git status -sb, and that is:

Now you might think this is insane, a single letter alias for an arbitrary git command? The reason is, at least in my personal experience, that this is the most common command I use out of all terminal commands. Here are the top 6 commands from my history:

1  648  6.48065%   s
2  640  6.40064%   cd
3  505  5.05051%   gc
4  410  4.10041%   vim
5  331  3.31033%   docker
6  292  2.92029%   ga
7  266  2.66027%   ls

You can create a similar statisic for yourself using the following command (reference):

history | awk '{CMD[$2]++;count++;}END { for (a in CMD)print CMD[a] " " CMD[a]/count*100 "% " a;}' | grep -v "./" | column -c3 -s " " -t | sort -nr | nl |  head -n10

At least in my case s is a winner and beats even cd, followed by gc (the git commit -v from before). This makes sense, because every time you would want to make a new git commit, you’d cd into the directory and check the status.

This statistic (at least for my git usage) confirms that git status -sb is not just a random command, it is the command, and as such it deserves a special alias. Some people suggest using alias gs="git status -sb", which might be leaning on the safe side, and I definitely started out that way. But is there really anything else that would deserve the glorious one letter s alias than git status -sb?

git add

Git separates the working tree (the files being edited) from the index, that is the staged changes which are ready to be comitted. Before using git commit, we have to stage our changes using git add. But since this doesn’t have to include all of the changes, git add comes with quite a few options (check man git-add).

For example, git add -u only stages files that were modified (or deleted), but not newly added files. To add all modifications, deletions and additions, one can simply run git add . But sometimes we want to be more granular than adding whole files. This is where git add -p (or --patch) comes in, which launches an interactive mode, prompting the user with each change whether they want to add it to the index.

Aliases:

git branch

Branching is an integral art of git, and as such the git branch command deserves at least one alias of its own. Lucky for us we can rely on git checkout for most of the branching shenanigans, and as such we only mention git branch --all. Deleting branches is sometimes useful as well, but as there are multiple ways - with some people preferring -d and some -D - we leave this out of the aliases.

git commit

Creating new commits is one of the most common operations when using git, and as such we want to have a decent setup for it. Firstly, git commit has a -v flag which causes it to show a complete diff when it opens the editor for the commit message.

One benefit of using git commit -v as opposed to git commit -m "Some message" is that you get one last chance to inspect what is being changed. This might be as simple as holding down Ctrl-D to scroll down in Vim in a matter of seconds, but there are certainly times where such quick visual inspection can catch unexpected files being committed (especially when the size of the diff is much larger/smaller than expected, or weird characters pop up). As such, we’ll use -v as a default option for all our git commit commands.

We can also use -a to automatically stage all modified files before committing (similar to git add -u).

Lastly, there is the --amend options, which gives us a way to fix the last commit if it hasn’t been pushed. Since git history is immutable, this does not actually change the commit, but instead creates a new one and resets HEAD to it. This is critical information, because it means you should not --amend after you used git push. That is after some other computer contains the old commit. If you do git commit --amend after pushing, you will be required to force push (git push --force), as your tree is not a simple extension of the tree on the origin server, and needs to be overwritten by your local copy. This is similar to using git rebase or git reset --hard as we’ll see later. In simple terms, if you git push --force you’re in some sense overwriting origin, and if someone else ran git fetch (or git pull) in the meantime, they will have a different local tree that won’t be compatible with origin after you for push anymore. There are definitely ways to work around these problems, but none of them are trivial, and out of the scope of this article.

If you’re interested in a followup article covering git push --force or some other topic, feel free to leave a comment below the article or message me on twitter.

git cherry-pick

Cherry picking allows us to copy (apply) arbitrary commits to our HEAD. While not a common operation it does come in handy from time to time, especially when fixing previous git-related issues. As cherry-picking is very problem specific, we don’t introduce any default flags and only use the most basic alias

git checkout

Git checkout is our first command that directly manipulates the working tree. First we introduce git checkout <BRANCH> as a way to switch between branches. We can also use this to switch our working tree to an arbitrary commit in the history (as in git checkout <REF>). Surprisingly to some, git checkout is better at creating new branches than git branch is, as it allows us to create the branch and switch to it with a single command git branch -b <BRANCH>.

Lastly, we can use git checkout <FILE> to discard its changes and revert it back to the version in the index, or in case nothing is staged, to HEAD. A small example to illustrate this where we create and commit a new file, then make some changes to it, stage them, make some more changes, use git checkout to reset the unstaged changes, then use git reset to clear the stage, and again git checkout to reset the remaining changes.

One sidenote, consider if we had a file named master while also having a branch named master. If we wanted to use git checkout on the file and wrote git checkout master, git would actually refer to the branch instead. For this reason git provides a -- argument, which tells git to process the comes after it as filenames. This means git checkout -- master will apply git checkout on a file named master. It’s a good idea to use this option every time you want to apply git checkout to a file, as you might forget you have a branch with the same name and get confused about the results (especially if you create lots of temporary branches/files named a or foo or test). For this reason we’ll use git checkout -- file.txt as opposed to git checkout file.txt.

$ mkdir checkout-demo
$ cd checkout-demo
$ echo aaa > file.txt
$ git init
Initialized empty Git repository in ~/checkout-demo/.git/
$ git add .
$ git commit -m "Initial commit"
[master (root-commit) 52ae6f1] Initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 file.txt
$ echo bbb >> file.txt
$ git status -sb               #
## master                      #
 M file.txt           # -------- here the ` M` is
$ git add file.txt             # in the right column meaning
$ echo ccc >> file.txt         # unstaged changes only
$ git status -sb               #
## master                      #
MM file.txt           # -------- here we have `MM` for
$ cat file.txt                 # both staged and unstaged
aaa                            # changes
bbb                            #
ccc                            #
$ git checkout -- file.txt     #
$ git status -sb               #
## master                      #
M  file.txt            # ------- and finally here the `M `
$ cat file.txt                 # is on the left, signifying
aaa                            # staged changes only
bbb                            #
$ git reset                    #
Unstaged changes after reset:  #
M       file.txt       # ------- this is not what we *have after*,
$ git status -sb               # but what *was changed*
## master                      #
 M file.txt             # ------ confirming the output,
$ git checkout -- file.txt     # `git reset` unstaged our changes
$ git status -sb               # and now we're back to ` M`
## master
$ cat file.txt
aaa

The code examples are intentionally a bit verbose to keep them reproducible. You can simply follow along command by command in your own terminal. Playing around with git is a great way to learn!

If you found this example confusing due to the git status -sb outputs not being correctly highlighted, or are confused in general, here’s the same thing but in a properly colorized screenshot

colorized git checkout with git status -sb

As noted before, yellow means staged, green means unstaged, and git checkout modifies the unstaged changes, meaning it removes the green ones.

git diff

Similarly to git status, checking the changes made to the working dir is a very common operation. The git diff has a useful flag which I would recommend using by default, and that is git diff -M. This will allow git diff to detect when a file was renamed. If you rename a file and don’t use -M, git diff will show one file as deleted and another one as newly added, as opposed to showing it as a rename.

Note that this option does not work 100% of the time, because git can’t know with certainty that a file was renamed. The filesystem doesn’t keep any kind of log of rename operations, nor is it written in any kind of metadata. Git will simply look at the contents of the two files, compare them, and if the amount of changes is small enough, it will consider the file as renamed. You can actually specify the threshold for changes with the -M flag, specifically -M90% would tell git to only consider something to be renamed if more than 90% of the file hasn’t been changed. By default this is set to 50%. This might seem silly at first, but consider renaming a file in your editor and then making changes to it. You wouldn’t necessarily commit things right after the file was renamed, yet you would still expect git to track the rename.

Personally I use git diff so often I devoted a second one-letter alias to it, specifically just d as opposed to gd. That is the following:

As opposed to alias s="git status -sb" I’d say this one is more debatable, considering git diff is used much less often than git status.

There is one more useful flag with git diff, and that is --cached. The regular git diff only shows diff between the working directory and the index. That is changes you could possibly stage with git add. But sometimes you might have already staged some changes while leaving others unstaged (say with git add -p) and would like to see only the changes that would be comitted. This is where git diff --cached comes in, which will show the difference between your staged changes and HEAD.

An honorary mention goes to the --word-diff flag, which is not common enough to make an alias, but still useful to know about and I suggest playing around with it (and also look it up in man git-diff). By default git diff will show how the whole line has changed, but with --word-diff it will try to show diff within the line itself (on single words). This can be useful when making small tweaks to long lines, such as READMEs or documentation, but less so on code (which is why it’s not the default).

git fetch

Sometimes people are surprised when they see me using git fetch followed by a git merge --ff-only and they ask why even bother running git fetch when you could just git pull, right? The problem with git pull is that it essentially does two things at once. It fetches the changes from the remote, and then merges (or rebases, depending on the flags) your HEAD with the changes, with the issue being that you don’t see what has changed before it begins with the merge.

Automating the merge is fine if it is what you actually wanted to happen, and for this reason I don’t think git pull is inherently a bad command. But more often than not I find people get surprised by the result of the pull, as it does something to their local copy that they didn’t expect. For that reason alone it might be useful to consider using a combination git fetch, looking at the changes with git log (as we’ll see shortly), and then manually merging/rebasing as needed.

By default git fetch will only fetch the tracking remote, but there is also the --all option which will fetch from all remotes.

git log

Now perhaps one of the commands which forces many people to use GUIs instead of the terminal interface to git. They simply can’t find useful information from the default git log output, and I don’t blame them. Personally I can’t use git on a computer without a proper git log alias as well, and often resort to either searching github for my own git log alias, or just use a GUI.

There are essentially two (or three) parts to making git log usable. One is forcing it to only print out one line per commit, which can either be done using --oneline, or a custom format (as shown shortly) with --pretty. The second is --graph, which visually represents branches and history. The third, and optional, is --all, which shows the history for all branches, not just the past of where HEAD is pointing to.

All of this combined is git log --graph --oneline --all, which when applied to Facebook’s React at the time of writing this article looks like this:

git log on react repo

While this output is very useful and already infinitely better than the default, it does not display the author, when the commit was made, and doesn’t give us a way to customize its colors (which might be important if you have a custom color scheme).

Thus I present to you the full version of the --pretty version.

git log pretty on react repo

The above screenshot was created with the following command:

git log --all --graph --pretty="format:%C(yellow)%h%C(auto)%d%Creset %s %C(white) %C(cyan)%an, %C(magenta)%ar%Creset"

It looks complicated, but if you look at it for a few seconds you can see it really isn’t. We’re just passing in a format string with two different kinds of placeholders. One is the colors, e.g. %C(yellow) or %C(auto), and the others are the actual content, such as %h or %s. You can certainly customize the command to your liking, and I suggest you look at man git-log in the PRETTY FORMATS section which lists all of the possible format string options.

Because sometimes we want to view the history of the current branch only, and sometimes we want to see all of it at once, we’ll resort to two aliases, differing only with the use of --all.

git merge

Since branches are an integral part of git, merging branches is equally if not more important. There are two important kinds of merges, fast-forward and with a merge commit. Fast-foward means the current branch set to the ref in which it is being merged into. For example, if we’re fast-forward merging master into origin/master and we have no pending changes in the working directory, this is essentially equivalent to running git reset --hard origin/master, as the master label simply changes to a commit further in the history. But sometimes the target ref is not directly ahead and might be lying on a parallel branch, in which case git will create a merge commit that joins the two branches together.

There are three important flags which should be paid attention to:

Most problems arising during git usage come from git doing something that the user did not expect, which is why the first option (--ff) can be quite dangerous. You simply don’t know if a fast-forward will happen or if a merge commit will be created unless you examine the history and are certain how fast-forward works.

You might think that git will be able to fast-forward, run git merge --ff, and be surprised by the result and maybe have to revert it, or only realize it later and have even more work to fix. This is why I suggest to never use the default --ff option, but instead be explicit and either use --ff-only or --no-ff.

The benefit is that --ff-only is somewhat safer and thus you can run it without that many worries to basically check if a fast-forward is possible. If it’s not, the command will simply fail, and you can decide if you want to run the --no-ff version instead, or maybe re-examine the history and figure out why it failed.

git push

There’s not that much to be said about git push, other than the --tags flag which causes git to push all of the tags to the remote repo, meaning you can do git push --tags origin instead of git push origin <tag name>.

git reset

Manipulating the HEAD with git reset is one of the lesser understood commands, and can lead to some potentially dangerous situations. It has multiple modes, the three most useful ones are the following (all examples assume your working dir is clean and you have no pending changes):

One last worthy mention is git reset --patch, which similarly to git add --patch (or -p) will ask you which changes you want to keep staged and which unstaged, and then reset the HEAD.

The respective suggested aliases are:

git rebase

Similarly to git reset, the git rebase command is one of the lesser understood but more dangerous commands. Some people use it to edit the history, but that’s not really what it does. The git history tree is immutable, and we can only ever add new commits to it, which is exactly what git rebase does. It will copy and re-apply existing commits in a different place, possilby modifying them along the way.

Let’s go through the example presented in the git rebase manpage (available at man git-rebase):

Assume the following history exists and the current branch is “topic”:

          A---B---C topic
        /
    D---E---F---G master

From this point, the result of either of the following commands:

git rebase master
git rebase master topic

would be:

                  A'--B'--C' topic
                /
    D---E---F---G master

At first glance it might seem that git rebase somehow took the commits A, B, and C and moved them over to start at G, but if you look more closely you can see that they were renamed A', B' and C'. This is very much intentional, because git rebase is not moving the commits, it is simply creating new ones in a different place. The original commits are still in the tree, they are just not visible because nothing is pointing to them. If we were to write down the ref of C prior to doing the rebase, then ran the rebase, and ran git tag REF, it would re-appear in the history as if by magic. That’s because it was there all along, git rebase only created its copy on top of G.

The same would happen if we ran git rebase -i HEAD~5, that is interactively rebase last five commits. Sometimes people would do this to squash commits before submitting a pull request. It is important to note that this does not replace those five commits with a new one, it only creates one new commit with the contents of the five, and resets the branch label (and HEAD) to it, making it appear as if the original commits were edited.

While rebase is replaying commits it might run into a conflict, in which case it will stop and ask the user to resolve the conflict. After the conflict is resolved, the rebase can continue with git rebase --continue. As this situation is quite common, we devote an alias to it as well.

A word of caution: If you decide to use git rebase on a production codebase, I suggest you take a lookt at git reflog first (man git-reflog) and play around with how different rebase variants get stored in the reflog so you can recover when things go wrong.

git remote

Listing and managing remotes is a common practice in any git-controlled project, even if it just means pushing to GitHub (or other). Sometimes we might not be certain where origin is, and that is where git remote -v comes in handy, as it will simply print out the list of remotes.

git stash

There’s not that many things worthy to mention with git stash, especially since you could accomplish the same using temporary branches and cherry-picking. But sometimes the changes are small enough to warrant the use of git stash instead, and that’s why we introduce a few handy aliases:

git show

Last command on the list is git show, which simply tells git show me what changed in this commit (including the author, date, and commit message in detail). This might be useful both when checking what the last commit was (either git show HEAD or just git show without an argument), or when looking at a particular commit in the past.

The alias gw might not be what you immediately think of, but since other gs-prefixed ones are taken, it is at least somewhat phoentically resemblant of the command itself.

Conclusion

If you’ve read this far I hope you learned at least a thing or two. Git is a massive tool with hundreds of useful flags and sub-commands and there are definitely times where having that one extra trick up your sleeve can save you hours of pain. If you have any tips, suggestions, corrections, or feedback, please do leave a comment below or hit me up on twitter. I’ll be sure to reply to each and every comment.

Here is a complete list of aliases mentioned in the article, ready to be copy-pasted into your ~/.bashrc or ~/.zshrc or wherever else you store your aliases (sorry fish users).

alias s="git status -sb"
alias ga="git add"
alias gau="git add -u"
alias gap="git add -p"
alias gb="git branch"
alias gba="git branch --all"
alias gc="git commit -v"
alias gca="git commit -v -a"
alias gcam="gca --amend"
alias gch="git cherry-pick"
alias gco="git checkout"
alias d="git diff -M"
alias gdc="git diff -M --cached"
alias gf="git fetch"
alias gfa="git fetch --all"
alias gl='git log --graph --pretty="format:%C(yellow)%h%C(auto)%d%Creset %s %C(white) %C(cyan)%an, %C(magenta)%ar%Creset"'
alias gla='gl --all'
alias gm="git merge --no-ff"
alias gmf="git merge --ff-only"
alias gp="git push"
alias gpt="git push --tags"
alias gr="git reset"
alias grp="git reset --patch"
alias grh="git reset --hard"
alias grsh="git reset --soft HEAD~"
alias grb="git rebase"
alias grbc="git rebase --continue"
alias grbi="git rebase -i"
alias grv="git remote -v"
alias gst="git stash"
alias gstp="git stash pop"
alias gw="git show"
Related
Git · Linux