My favorite git aliases
I tend to use git from the command line frequently. There’s a useful feature to reduce repetitive work called git aliases:
If you don’t want to type the entire text of each of the Git commands, you can easily set up an alias for each command using
git config
.
Most examples are pretty trivial, e.g. being able to type git co
instead of
git commit
. I have autocomplete setup for my shell, so this breed of “type
fewer characters” alias doesn’t give me much value.
I use git aliases to automate some more complicated or repetitive operations. Here are my favorites.
Update: I’ve got some improved versions of these aliases at My favorite git aliases - revisited, check it out!
git glg
to summarize recent commits
I like a good overview of recent activity.
We can get a nice graph by passing a lot of arguments to git log
:
git config --global alias.glg 'log --oneline --decorate --all --graph'
Git will render a nice, colorful chart of what’s going on locally and on the server.
This helps me when I’m:
- splitting my work into small branches (for easier review)
- getting up to speed
- planning an interactive rebase
git amend
to add content to the most recent commit
I rarely get things perfect the first time. I’ll often make a commit (with a good commit message), and then realize I made some mistake: forgot to save a file in my editor, forgot to remove some debugging code, spelling error, etc.
This alias gives me a quick way to fix that, without adding any noisy commits
that would make life harder for code reviewers or
git bisect
.
git config --global alias.amend 'commit --amend --reuse-message=HEAD'
Here’s how I use it:
# do some work, save some files
# stage my work
$ git add -p
# commit with a good commit message
$ git commit
# realize my mistakes, fix them, save the files
# stage my fixes
$ git add -p
# add the fixes to the existing commit
$ git amend
git changelog
to build pull request descriptions
Often I’ll have a few small commits bundled together for one pull request. I already have good commit messages, and can aggregate those together to make a good pull request description.
git config --global alias.changelog "log origin..HEAD --format='* %s%n%w(,4,4)%+b'"
This uses git log
pretty formats to
get a markdown listing of my pending commits. The crazy --format
string
handles indentation and line wrapping to keep the markdown valid.
$ git changelog
* <commit subject>
<line wrapped commit body, indented properly
for markdown lists>
* <next commit subject>
From there I copy/paste into github/gitlab. All these tools allow reviewers to see the individual commits, but this saves reviewers a few clicks and gives me more incentive to have good commit messages.
git workon
to get a fresh start
I use local branches just about every time I start a new task. I start with the latest codebase, and add my work on top. I spent a few years typing this in:
$ git fetch && git checkout -b new-feature origin/master
remote: Enumerating objects: 262, done.
remote: Counting objects: 100% (181/181), done.
... # more `git fetch` output
Branch 'new-feature' set up to track remote branch 'master' from 'origin'.
Switched to a new branch 'new-feature'
This is nice because I never have to worry about my local master
branch being
out-of-date, and I avoid a lot of git pull
, git merge
operations that I
always tended to get wrong somehow.
This approach stopped working for me when we started changing default branch
names and when I started contributing to projects using
git flow.
Remembering which origin
branch was the right one to work from became tedious.
We can automate it with a complicated alias:
git config --global alias.workon '!f(){ git fetch && git checkout -b "$1" $(git symbolic-ref refs/remotes/origin/HEAD | sed "s@^refs/remotes/@@"); };f'
There’s a lot going on here. The !
indicates this alias is an “external
command”, not an extension of one existing git subcommand. The command
accomplishes a few things:
- declare a bash function
f
to hold some sequential operations -
git fetch
to update my local.git
database with any activity from the server - use a subshell to run
git symbolic-ref
andsed
, figuring out the correct remote branch I should start from for this repo, usually something likeorigin/main
ororigin/develop
-
git checkout
a new branch named after the first argument off
, starting from that remote branch
I use this every time I start a task, and don’t have to think about per-repo branching conventions.
$ git workon new-feature
remote: Enumerating objects: 262, done.
remote: Counting objects: 100% (181/181), done.
... # more `git fetch` output
Branch 'new-feature' set up to track remote branch 'develop' from 'origin'.
Switched to a new branch 'new-feature'
git refresh
to integrate early
It can take me awhile to finish a task, and often other folks will have committed changes. I want to resolve any merge conflicts or integration issues early. I spent a few years typing this in:
git fetch && git stash && git rebase origin/master && git stash pop
Let’s walk through it:
-
git fetch
- update my local.git
database with any activity from the server -
git stash
- hide any uncommitted work -
git rebase origin/master
- replay any committed work on top of the latest version of the codebase -
git stash pop
- restore any uncommitted work
As with git workon
, this worked fine until I started working with other
branching structures. The solution is similar; another external command alias:
git config --global alias.refresh '!f(){ git fetch && git stash && git rebase $(git symbolic-ref refs/remotes/origin/HEAD | sed "s@^refs/remotes/@@") && git stash pop; };f'
This does the same git symbolic-ref
trick to find the right origin
branch
for the rebase.
Here’s an example run:
$ git refresh
No local changes to save
Current branch feature/add-a-thing is up to date.
Auto-merging Api/Controllers/SearchController.cs
On branch feature/payments
Your branch is up to date with 'origin/develop'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: Api/Controllers/SearchController.cs
Untracked files:
(use "git add <file>..." to include in what will be committed)
Api/Controllers/test
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (c03c50a5d2e93639dc60091ddbe474a3e973ccb9)
With git refresh
I can be sure my changes are fast-forward, which I think
makes for easier code reviews.
git cleanup-merged
to remove old branches
I use branches all the time, and these can accumulate to the point where
git branch
returns hundreds of dead branches. These dead branches also clutter
up my git glg
output.
This alias removes any local branches that have been merged:
git config --global alias.cleanup-merged "!f(){ git fetch && git branch --merged | grep -v '* ' | xargs git branch --delete; };f"
Let’s break it down (ignoring the f
idiom):
-
git fetch
- update my local.git
database with any activity from the server -
git branch --merged
- show all branches reachable fromHEAD
-
grep -v '* '
- filter out lines that begin with a*
, which indicates the current branch -
xargs git branch --delete
- usexargs
to convert lines of output fromgrep
to positional arguments togit branch --delete
, and drop the local branches
Let’s see an example:
$ git cleanup-merged
warning: not deleting branch '5-follow' that is not yet merged to
'refs/remotes/origin/5-follow', even though it is merged to HEAD.
error: The branch '5-follow' is not fully merged.
If you are sure you want to delete it, run 'git branch -D 5-follow'.
warning: not deleting branch 'RyanBio' that is not yet merged to
'refs/remotes/origin/RyanBio', even though it is merged to HEAD.
error: The branch 'RyanBio' is not fully merged.
If you are sure you want to delete it, run 'git branch -D RyanBio'.
Deleted branch 34-devcontainer (was c48c632).
Deleted branch 38-overfetch (was 3207592).
Deleted branch github-links (was 0795fa6).
We’re getting some warnings from git branch --delete
where git thinks the
branch might not be all the way merged. This is good! I like safety measures on
destructive operations. From there I can dig in and see what’s going on and
decide to forcefully delete those branches if I care.
This does not modify anything on the git origin
at all, this is a 100% local
operation.
managing git aliases with ansible
Sometimes I have to work on different machines, and I like using
ansible
to help automate my customizations.
Here’s my task for setting up all these aliases, using the default
git_config
module.
- name: setup git aliases
community.general.git_config:
scope: global
state: present
name: "alias.{{ item.alias }}"
value: "{{ item.value }}"
with_items:
- alias: changelog
value: log origin..HEAD --format='* %s%n%w(,4,4)%+b'
- alias: glg
value: log --oneline --decorate --all --graph
- alias: amend
value: commit --amend --reuse-message=HEAD
- alias: workon
value: '!f(){ git fetch && git checkout -b "$1" $(git symbolic-ref refs/remotes/origin/HEAD | sed "s@^refs/remotes/@@"); };f'
- alias: refresh
value: '!f(){ git fetch && git stash && git rebase $(git symbolic-ref refs/remotes/origin/HEAD | sed "s@^refs/remotes/@@") && git stash pop; };f'
- alias: cleanup-merged
value: "!f(){ git fetch && git branch --merged | grep -v '* ' | xargs git branch --delete; };f"
Conclusion
Git aliases have value far beyond trivial abbreviations, and can help you be more consistent, accurate, and effective with your git.
Update: I’ve got some improved versions of these aliases at My favorite git aliases - revisited, check it out!