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.

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.

screenshot of git glg showing colorful output

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:

  1. declare a bash function f to hold some sequential operations
  2. git fetch to update my local .git database with any activity from the server
  3. use a subshell to run git symbolic-ref and sed, figuring out the correct remote branch I should start from for this repo, usually something like origin/main or origin/develop
  4. git checkout a new branch named after the first argument of f, 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:

  1. git fetch - update my local .git database with any activity from the server
  2. git stash - hide any uncommitted work
  3. git rebase origin/master - replay any committed work on top of the latest version of the codebase
  4. 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):

  1. git fetch - update my local .git database with any activity from the server
  2. git branch --merged - show all branches reachable from HEAD
  3. grep -v '* ' - filter out lines that begin with a *, which indicates the current branch
  4. xargs git branch --delete - use xargs to convert lines of output from grep to positional arguments to git 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.

Tags:

Updated: