Most engineers have git aliases; what are yours?
Summary: Typing is hard. Here are my git aliases.
I like typing on the command-line. Even after a decade of programming, it still makes me feel like a hacker. I'm not someone who needs a fancy GUI; I can type a few quick commands and bam!—my computer does something powerful. And it's fun to clicky-clack my way through a long CLI command.
But git
tests my limits. At first, it was fun to type out git pull --rebase origin master
or git push origin HEAD
.[1] And the first twenty times I was able to write nine git commands and get a commit cherry-picked onto a different branch, I felt pretty clever. After years of writing those exact same commands, I finally started putting them into git aliases.
And I'm not alone. Most engineers I've worked with have set up aliases for their own workflows, and it's always interesting to see how they've automated things. One of my old co-workers even includes delete commands in his aliases to make sure old branches are getting cleaned up appropriately. And he writes beautifully clear commit messages too. He's an inspiration.
The ability to set up aliases like this is one of the reasons I love the CLI; while a GUI might be easier at first, the CLI lets me build something that's perfect for me. I can combine git
with other CLI tools like fzf
or gh
to build the precise tool that meets my needs. The git aliases I'm going to share probably won't work for you, but they will hopefully inspire you to build your own tools that fit you perfectly.
Finally, if you're not yet comfortable with git, I'd stronly recommend against setting up many aliases like this, especially if you're copying someone else's aliases. I run dangerous commands that make sense for my workflow, but those same commands could easily create a nightmare for you, especially if you don't understand what they're doing.[2] Learn git first, figure out your own workflow, and only then start to build your own tools.
My git aliases
I don't actually use the git alias feature for any of these commands. I instead find it easier to set up commands by creating shell functions in my ~/.zshrc
. Aside from being easier to set up, if I'm ever pairing with someone, I don't want them to be confused about whether git go
is an actual git command or one of my custom commands; it's clear from the name that gitgo
isn't baked into git.
gitgo
: how I switch branches
If you've never used fzf, "the command-line fuzzy-finder," you're missing out. It's a beautifully fast tool to quickly filter anything you'd like: files, git branches, history, or processes.
function gitgo () {
git checkout $(git branch --sort=-committerdate | fzf)
}
git branch --sort=-committerdate
: list branches ordered by committer datefzf
: choose one of those branches (with fuzzy search!)git checkout
: then select that branch
gr
: how I pull to get up to date with master/main
git pull --rebase origin main
isn't terribly long to type, but it's frustrating to type master
/main
in a repo and realize that I've typed the wrong one. I now type gr
to pull and rebase against the correct one:
function gr () {
git pull --rebase $(git symbolic-ref refs/remotes/origin/HEAD --short | sed 's|/| |')
}
# note: if you have oh-my-zsh's git plugin installed, you'll need a different name for this (or to manually override it)
# because it conflicts with one of that plugin's aliases
--rebase
: I'm a strong believer in rebasing when pulling. I find that it creates a more useful history.git symbolic-ref refs/remotes/origin/HEAD --short | sed 's|/| |'
: will either returnorigin master
ororigin main
depending on what the trunk is called
gp
: How I push and automatically open up PRs
Anytime I push a branch, I always want to open up a pull-request so that CI/CD checks can start running. Github's CLI gh
makes it simple to open up the PR automatically.
function gp () {
# --force-with-lease is necessary because I regularly rebase
git push origin HEAD --force-with-lease
if gh pr view > /dev/null; then
return 0
fi
gh pr create --fill
}
Having an alias for pushing also gives you a good spot to add any extra checks that you'd like. As an example, while developing something, it's sometimes expendient to add an if true:
, an early return, or to comment out a block of code. When doing that, most folks will add a comment like // TODO: super dangerous! make sure to undo
, but it's possible to miss a todo like that! When I work on a repo, I like to add a quick search for DEV_ONLY
to the codebase's git hooks to short-circuit any pushes and update linting to fail any builds that include that string, so that I can make those changes with confidence.
If I were ever working in a repo where people disliked this practice, I'd probably edit my little script to include this check:
function gp () {
if git grep DEV_ONLY; then
echo "exiting because DEV_ONLY was present in the codebase";
return 1
fi
...
}
I wouldn't want to set this up as a global git hook because I quite like having repo-specific shared git hooks. For NodeJS projects, I normally use husky to set up repo-specific shared git hooks.
granch
: How I create new branches
When I create a new branch, I always want it to be based on the most up-to-date version of the primary branch of the repo:
function granch () { branch_name=$1
git fetch $(git symbolic-ref refs/remotes/origin/HEAD --short | sed 's|/| |')
git checkout -b "$branch_name" "$(git symbolic-ref refs/remotes/origin/HEAD --short)"
git push --set-upstream origin "$branch_name"
}
This would be shorter if I didn't work on repos that had both master
and main
as the primary branch.
git clean -fdx
: Have you tried turning it off and on again?
I don't actually have this one aliased, but it's one of my favorite git commands. It removes everything that git
doesn't know about from a repo, which is a great way to get back to a clean slate. Most repos have some amount of cached local data (built files, dependencies) and resetting that data can fix some problems.[3]
-f
:force
. You can also use-i
for interactive or-n
to log what would be removed.-d
: delete directories-x
: include hidden files
git_oops
: go back to the version of a file on the trunk
function git_oops () {
git fetch
git checkout $(git symbolic-ref refs/remotes/origin/HEAD --short) -- $0
}
This is another one that I don't actually have set up as an alias, because it's a short command and I'll sometimes want to choose the branch or ref to grab my files from.
More alias ideas
git_yoink
: Stick the last commit into a better spot
I'll often work on a branch and run into a small annoyance that I want to fix. I'd rather have that fix on a different branch; it means we can land it faster and have a conversation about that proposed change separately from the proposal of whatever else I'm working on.
I'll normally create a new branch based off of the primary branch of the repo, use git add -p
to commit only the fix, and then head back to the branch I'm currently working on. I'm testing out a new function that lets me write git_yoink branch-name-for-my-fix
that takes my last commit, plops it on to a new branch, and then gets me back to my current state:
function git_yoink () { new_branch=$1
# stashing local state
local should_pop_stash=false
git add .
if ! git diff --cached --name-only --exit-code; then
git stash push
should_pop_stash=true
fi
# grabbing the commit
commit=$(git rev-parse HEAD)
# for when things inevitably go wrong :)
echo "current state: commit=${commit}, branch=${git branch --show-current}, new_branch=${branch_name}"
git status
# granch "$new_branch"
git fetch $(git symbolic-ref refs/remotes/origin/HEAD --short | sed 's|/| |')
git checkout -b "$branch_name" "$(git symbolic-ref refs/remotes/origin/HEAD --short)"
git push --set-upstream origin "$branch_name"
# adding the commit on to the branch I created based on master
git cherry-pick "$commit"
# gp
git push origin HEAD
if command -v gh &> /dev/null && ! gh pr view > /dev/null; then
gh pr create --fill
fi
# go back to previous state
git checkout -
git reset --hard HEAD^
if [[ "$should_pop_stash" -eq "true" ]]; then
git stash pop
fi
}
Be careful with this function. I just started using it, so it's not well tested. And if it fails halfway through, you'll need to understand what it's doing well enough to get yourself back to a clean state. Like most of this code, I'm sharing it as for inspiration for the kind of things you can do—not as an example to copy!
Augmenting and simplifying git commit
When I git blame
, I often find that I was the villian who wrote hundreds of lines of abstruse code accompanied only by a myterious commit message like "perf improvement" or "fix messaging bug." Past-Will was a monster. Over the years, I've gotten better at including more of the details necessary to recreate my thought process, but it's something that takes discipline.[4] I keep meaning to semi-automate it for myself by pulling current project info and including that project info on my commit. Here's a quick example of what that might look like if I were to pull data from Asana:
ASANA_ACCESS_TOKEN="2/super_secret_pat" # https://developers.asana.com/docs/personal-access-token
ASANA_PROJECT_ID=123456
# I found asana's API annoying to work with, so this doesn't filter to my personal tasks or anything like that
function get_asana_tasks () {
# to find SECTION_ID: `curl -s -H "Authorization: Bearer $ASANA_ACCESS_TOKEN" "https://app.asana.com/api/1.0/projects/$PROJECT_ID/sections" | jq .`
# I can never remember jq's syntax, even with LLM help, so I use https://www.npmjs.com/package/node-fn-query
# compared to jq, it's worse in every possible way except that it uses NodeJS syntax I can actually remember
local SECTION_ID='456789'
curl -s -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" "https://app.asana.com/api/1.0/tasks?section=${SECTION_ID}" |\
jq -c -r '.data[]' |\
nq -o '({ gid, name }) => `https://app.asana.com/0/123456/${gid} ${name}`'
}
function git_commit () { local message=$0
asana_deets=$(get_asana_tasks | fzf)
git commit -m "${message}\n ${asana_link}"
}
If we wanted to take this even further, we could even use an LLM to explain the diff of the commit in the context of whatever project management tool and docs we've linked to. It wouldn't surprise me if there's already a startup out there that charges $10/developer/month that's trying to provide beautiful git commits as a service.
And for what it's worth, this is probably about the point that I'd switch from writing this in the shell using a language like python or NodeJS. There's no reason to limit ourselves to bash/zsh when building personal tools!
Watch github builds
To watch the result of CI builds, I normally use a custom info-radiator. But it's pretty straightforward to build something simple to keep an eye on the status of your branches. If you use github actions, here's what a terrible version of a CLI-based info-radiator might look like:
# I don't think this will get you rate-limited, but I haven't tested it
# it also relies on nq (npm install -g node-fn-query) because I'm too lazy to write this only using jq
function gwatch () {
workflow_name=$(gh workflow list --json path | jq -r '.[] | .path' | sed -r 's|.+/||' | fzf)
me=$(gh api user -q .login) # you can hardcode this if you'd like :)
while true; do
clear
gh run list --workflow "$workflow_name" -u "$me" --json createdAt,status,conclusion,headBranch |\
nq '_.flow(_.orderBy("createdAt", "desc"), _.uniqBy("headBranch"), _.map(c => `${c.headBranch}: status: ${c.status} conclusion: ${c.conclusion}`))' |\
jq -r '.[]'
sleep 60
done
}
Just show me the code
If you've read the rest of this post, there's nothing new here! Just adding this section for folks who want to jump to some code first.
function gitgo () {
git checkout $(git branch --sort=-committerdate | fzf)
}
function gp () {
git push origin HEAD --force-with-lease
if gh pr view > /dev/null; then
return 0
fi
gh pr create --fill
}
function gr () {
git pull --rebase $(git symbolic-ref refs/remotes/origin/HEAD --short | sed 's|/| |')
}
function granch () { branch_name=$1
git fetch $(git symbolic-ref refs/remotes/origin/HEAD --short | sed 's|/| |')
git checkout -b "$branch_name" "$(git symbolic-ref refs/remotes/origin/HEAD --short)"
git push --set-upstream origin "$branch_name"
}
That was a lot of words for a pretty small block of code, huh?
And then type
git pull --rebase origin main
because this repo usesmain
rather thanmaster
. ↩︎But if you do mess things up, don't lose hope! The
reflog
command can let you fix almost any mistake: ohshitgit.com. The "ref log" gives you a log of all previous states that you can go back to. ↩︎In an ideal world, you'd be able to figure out what local state went sideways and fix whatever went wrong permanently! But it's good to give people an option to do the repo equivalent of "have you tried restarting your computer?" ↩︎
A while ago, I chatted with a founder of a startup that was working on turning code-bases into an AI-backed knowledge base that you could ask questions to. It sounded pretty cool! She came from a Jira-heavy background where every commit and change had to be linked to Jira tickets; the idea that a large change might only have a message like "fix bug on login" horrified her. And she was totally right; commits should have far more information than that, but best practices are hard to follow 100% of the time unless that best practice is automated. ↩︎