Git learnt
April 2, 2023•1,541 words
(ya know, if you want)
Git is a powerful tool, and by far the most effective way to unlock its potential is to use it on the command line. There's just one issue with that, which is that the syntax for Git commands is unintuitive, often overly verbose, and written at a very low level, so doing many operations you care about (like, say, "put me on the latest version of this specific branch from the remote") tend to involve several commands. And since the commands are often context-dependent, common operations may require different commands depending on the state of your local git.
So it's understandable that there are so many Git UIs, however, what I've found over the years of working with Git on the command line is that by setting aliases and local functions for common workflows, I can accomplish just about everything I need in short, simple commands. Here are some of those aliases, along with explanations of how they work.
All of these commands live in a GitHub repo where I've been collecting them, so you should be able to either clone it or copypaste them out to get them to work locally. Also, all the shell aliases and functions assume you're using a Unix-like shell (I use Git Bash even on Windows) but I'm sure if you really wanted to, you could port them over to something else like Powershell.
Put me on the latest version of this branch
This is one that somehow takes up to 3 separate commands in native Git, which is ridiculous because it's a core part of anyone's workflow. The alias I use is gfl
(git-fetch-latest) and I have it configured to either take a branch name as an argument, and put you on the latest revision of that target branch, or without a branch name it puts you on the latest version of your current branch. Super useful for code reviews!
gfl() {
if [ $# -eq 0 ]; then
git pull
else
git fetch && git checkout "$1" && git pull
fi
}
Usage:
$ gfl branch-name #pulls the latest version of branch-name from origin
$ gfl #with no arguments, pulls the latest version of the branch you're already on
Start a new branch from the commit I'm on
This is a common operation but of course the branch commands are janky and unintuitive (IN WHAT UNIVERSE DOES IT MAKE SENSE FOR git checkout -b
TO CREATE A BRANCH AND git branch -D
TO DELETE ONE). So you can either memorize the long form of this syntax, or make a short little alias (gcb
for git-create-branch) and not have to think about it. Note that any existing local changes will not be affected by this command.
gcb() {
git checkout -b "$1"
}
Usage:
$ gcb new-branch-name
Start a new branch from a specific tag
The workflow at my job involves using tags to mark specific builds, and mostly when we create new feature branches, it's those tags that we branch off. This is another one that's a fairly straightforward command in Git but it has a weird and verbose syntax, so I alias it to gbt
for git-branch-tag.
gbt() {
git fetch && git checkout tags/$1 -b $2
}
Usage:
$ gbt target-tag new-branch-name
Merge a specific tag into your branch
Sometimes at my job, the build we're targeting our feature branches against changes. When that happens, we should merge that newer commit (represented by a tag) into the feature branch we're working on. I use gmt
for git-merge-tag to do this.
gmt() {
git fetch && git merge $1
}
Usage:
$ gmt target-tag
Blow away all local work
Sometimes you're just having one of those days where you modify 18 files and then you realize it was all completely wrong. Right? It's not just me who has those days? Well, anyway, if you need to just wipe out every local change, you can use grhh
(git-reset-hard-head). This is another one where I memorized the syntax years ago and then realized I didn't like typing the whole thing out every time I need it, but as usual the syntax is janky and awkward and there's no reason you should have to remember such a specific series of flags for such a basic operation.
alias grhh='git reset --hard HEAD'
Usage:
$ grhh #just make extra sure you don't have any local work you want to keep!
Make your submodules work
If you're dealing with submodules, it can be kind of a pain to keep them in a correct state, especially since operations that change your local branch don't touch the submodules. This is (again!) a verbose one-liner, but running it will get your submodules into exactly the state that your current branch expects them to be in--even if they're not checked out at all, or if they're nested. If you're dealing with submodules that change a lot, you may want to work this alias into the commands you use to switch branches. It's gsm
for git-sub-module.
alias gsm='git submodule update --init --recursive'
Usage
$ gsm
Commit and push all your local work
A lot of the time when I'm working on a feature branch, I get to a good stopping point and I just want to push all my local work remotely. To do this in Git natively takes 3 commands, of course, but as you may have gathered, I hate typing any more letters than I absolutely must. So I set up gacp
for git-add-commit-push to let me just enter a commit message and send it all upstream in one operation. The one caveat with this is that sometimes you do have changes on your local that you don't actually want to go up to the remote; for that situation I use the next command.
gacp() {
git add . && git commit -m "$1" && git push
}
Usage
$ gacp "this is my sweet commit message"
Commit and push all your staged local work
If you have a bunch of local changes that you want to push, and some you don't, you can use this command to commit and push just the changes you've already staged -- use the git add
command to stage, and you can check the output of git status
(aliased below!) to see what is and isn't staged already. I call it gcp
for git-commit-push.
gcp() {
git commit -m "$1" && git push
}
Usage
$ gcp "this is my sweet commit message"
Pull from the remote, but without any weird error messages
If you have created a local branch and pushed it remotely, and then someone else has put newer commits on it, and you try to pull those commits down, Git is liable to spit out an annoying error message at you and fail. This is because technically the local<->remote linkage in Git works in two different directions, and by pushing your branch up from local, you do not default to setting it as the upstream target for when you do a pull. I'm sure there are complicated technical reasons that it works this way, but also I don't care because it's a dumb way for Git to work.
Anyway, I set up this alias (gpd
for git-pull-down) that lets you just pull from the remote branch matching the name of your local branch without worrying about any of this, and also save some typing of course.
gpd() {
head="$(git rev-parse --abbrev-ref HEAD)"
if [[ $(git config "branch.$head.merge") ]]; then #there's already a merge target configured, just pull as normal from there
git pull
else
if [[ $(git ls-remote --heads origin $head) ]]; then #there is an upstream branch existing with the same name as our branch
git branch --set-upstream-to origin/$head #set merge target to upstream branch with same name
git pull
else #fail with explanation
echo "Branch $head has no upstream or merge target! You will likely have to push first, or manually configure it"
return 1
fi
fi
}
Usage
$ gpd
Print a diff of all locally changed files
This is actually one that's really easy to write and remember but I hate typing and I run it all the time, so I've aliased it down to gd
for git-diff. Also I use diff-so-fancy to make the output of my diffs look frickin sweet and I suggest you do the same.
alias gd='git diff'
Usage
$ gd
Print the current status of your repo
Once again this is an easy one that I'm just too lazy to type out each time. I have it aliased to gs
for git-status.
alias gs='git status'
Usage
$ gd
Hopefully these commands save you some typing, and also having to memorize some arcane and obnoxious syntax. My brain and my fingers have very little tolerance for either, so having a bunch of 2- or 3- character commands for the most common operations I run in Git directly improves my workflow, my happiness, and my sanity.