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 -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:
alias s="git status -sb"
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:
alias ga="git add"
alias gau="git add -u"
alias gap="git add -p"
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.
alias gb="git branch"
alias gba="git branch --all"
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.
alias gc="git commit -v"
alias gca="git commit -v -a"
alias gcam="gca --amend"
- Here you might want to opt out of the-a
depending on your preference.
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
alias gch="git cherry-pick"
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
As noted before, yellow means staged, green means unstaged, and git checkout
modifies the unstaged changes, meaning it removes the green ones.
alias gco="git checkout"
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.
alias gd="git diff -M"
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:
alias d="git diff -M"
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
.
alias gdc="git diff -M --cached"
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.
alias gf="git fetch"
alias gfa="git fetch --all"
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:
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.
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
.
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'
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:
--ff
(default): tries to perform a fast-forward, and if it can’t it will create a merge commit--no-ff
: creates a merge commit every single time, even if a fast-forward is possible--ff-only
: tries to perform a fast-forward and exits when if it is not possible
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.
alias gm="git merge --no-ff"
alias gmf="git merge --ff-only"
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>
.
alias gp="git push"
alias gpt="git push --tags"
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):
git reset --soft
: Moves theHEAD
, but leaves the index and working dir as is, meaning if you rungit reset --soft HEAD~
you’ll be in the state right before the last commit you made. Meaning all your changes are already staged.git reset --mixed
(default): Resets the index but leaves the working dir as is. As this is the default, the most common use case is to just rungit reset
without any arguments (which is exactly the same asgit reset --mixed
), which will unstage all of your changes, but leave the working dir intact.git reset --hard
: My favorite variant, and also the most dangerous one. This resets everything to the specified commit, including the working dir. If you rungit reset --hard
with no target, it will reset all of your staged and unstaged changes toHEAD
, essentially saying please discard all the changes I have made. If you specify a target, saygit reset --hard HEAD~
it means please discard all changes and setHEAD
and my current branch to one commit ago, which can be useful if you made a commit and want to discard it completely (just be mindful and don’t this if you’ve already pushed your changes, or if you’re not sure what you’re doing in general).
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:
alias gr="git reset"
alias grp="git reset --patch"
alias grh="git reset --hard"
alias grsh="git reset --soft HEAD~"
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.
alias grb="git rebase"
alias grbc="git rebase --continue"
alias grbi="git rebase -i"
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.
alias grv="git remote -v"
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:
alias gst="git stash"
alias gstp="git stash pop"
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.
alias gw="git show"
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"