Skip to content
Field Notes
Go back

Git Aliases That Go Beyond Shortcuts

Most developers know git config --global alias.st status exists. Fewer know that git aliases can run arbitrary shell commands, accept positional arguments, wrap multi-step workflows, and implement operations that don’t exist as native git subcommands. The gap between a developer who uses aliases well and one who doesn’t is one of the most visible daily productivity differences on a team.


TL;DR

Git aliases stored in ~/.gitconfig support an ! prefix that switches execution from a git subcommand to a full shell command. Combine that with anonymous shell functions and you get argument passing, conditionals, and multi-step workflows — all invocable as git <alias>. A small investment in a well-curated alias collection pays back on every project.


Context

Git’s built-in alias mechanism is documented but undersold. The official docs describe it as a shortcut system. In practice, it’s closer to a personal CLI extension layer. Every time you reach for an external script, a shell function, or a Makefile target to wrap a git workflow, there’s a good chance a git alias would be the right tool — one that lives with your git config, travels with your dotfiles, and requires no extra tooling.

The two common failure modes: developers who never set up aliases at all, and developers who set up a handful of two-letter shortcuts and stop there. Both leave significant friction on the table.


Analysis / Key Findings

Basic Aliases: The Obvious Starting Point

The simplest aliases map short names to git subcommands:

git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch

These get stored in ~/.gitconfig under an [alias] section:

[alias]
    st = status
    co = checkout
    br = branch

And they work exactly as you’d expect:

git st
git co main
git br -a

Any arguments you pass get forwarded to the underlying command. Simple, useful, but nowhere near the ceiling.

The ! Prefix: Shell Execution

Any alias value that starts with ! is executed as a shell command rather than a git subcommand. This is the boundary between shortcuts and actual tooling.

# Pretty graph log
git config --global alias.lg "log --oneline --graph --decorate --all"

# Show last commit with stats
git config --global alias.last "log -1 HEAD --stat"

# Arbitrary shell command
git config --global alias.hello '!echo "hello from git alias"'

The ! prefix hands control to /bin/sh, which means you have access to pipes, conditionals, environment variables, and anything else available in a POSIX shell.

Aliases with Arguments

Shell aliases receive all forwarded arguments as $@. For simple cases this is enough:

# Pickaxe search: find all commits touching a string
git config --global alias.find '!git log --all -S'
# Usage: git find "functionName"

For more control — especially when you need named positional parameters — wrap the logic in an anonymous shell function:

# Delete a branch locally and remotely
git config --global alias.delete-branch '!f() { git branch -d "$1" && git push origin --delete "$1"; }; f'
# Usage: git delete-branch old-feature

# Push current branch and set upstream in one step
git config --global alias.publish '!f() { git push -u origin "$(git branch --show-current)"; }; f'

The !f() { ... }; f pattern is the standard idiom. The function definition followed by immediate invocation ensures $1, $2, etc. bind to the arguments you actually passed, not to any outer shell context.

A High-Value Alias Collection

These are the aliases worth having in every ~/.gitconfig. All copy-pasteable.

# Visual branch graph — the most universally useful alias
git config --global alias.lg "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --all"

# Files changed in the last commit
git config --global alias.changed "diff-tree --no-commit-id -r --name-only HEAD"

# List all defined aliases
git config --global alias.aliases "config --get-regexp alias"

# Amend last commit without touching the message
git config --global alias.amend "commit --amend --no-edit"

# Undo last commit, keep changes staged
git config --global alias.uncommit "reset --soft HEAD~1"

# Branches sorted by most recent commit
git config --global alias.recent "branch --sort=-committerdate --format='%(committerdate:relative)%09%(refname:short)'"

# Stash with an automatic timestamp as the name
git config --global alias.save '!git stash push -m "$(date +%Y-%m-%d_%H:%M:%S)"'

# Sync a fork with upstream/main
git config --global alias.sync '!git fetch upstream && git rebase upstream/main'

# What you're about to push (not yet on remote)
git config --global alias.outgoing "log @{u}..HEAD --oneline"

# What came in from the remote (not yet local)
git config --global alias.incoming "log HEAD..@{u} --oneline"

git outgoing and git incoming are particularly useful before and after a git push / git pull — they answer “what exactly is about to move?” without any mental overhead.

Local vs. Global Scope

Global aliases live in ~/.gitconfig and apply everywhere:

git config --global alias.lg "log --oneline --graph"

Local aliases live in .git/config for the current repository:

git config alias.deploy "!./scripts/deploy.sh"

Local aliases are the right tool for project-specific workflows: deployment scripts, migration runners, environment-specific operations. They don’t pollute your global config, and they live alongside the repo so the whole team could adopt the same local config pattern via a shared setup script.

Gotchas Worth Knowing

Working directory: Shell aliases (!) always execute from the repository root, regardless of where your terminal is within the repo. If your alias references a relative path like ./scripts/deploy.sh, it resolves from the root — which is usually what you want, but worth knowing explicitly.

Tab completion: Aliases are not tab-completed by default in most shells. Bash users need bash-completion configured with git support; zsh users get this automatically with the git plugin in oh-my-zsh or similar. Without it, you type aliases blind — which is fine once they’re muscle memory, but worth setting up early.

Quoting: Single quotes and double quotes behave differently in shell aliases. Use single quotes when the alias value should be taken literally (no variable expansion at definition time). Use double quotes when you intentionally want expansion at definition time — which is rarely what you want.

Viewing Your Aliases

# See all aliases in global config
git config --global --get-regexp alias

# Or with the alias we defined above
git aliases

Conclusion

Git aliases cross from “nice to have” into “genuinely useful” the moment you adopt the ! prefix and the function wrapper idiom. The basic form covers abbreviations. The shell form covers arbitrary workflows. The function form covers argument passing. Together, they cover the large category of repetitive multi-step git operations that every developer otherwise handles with ad hoc shell scripts, muscle memory, or documentation lookups.

The aliases worth setting up first: lg (graph log), uncommit (soft reset), amend (no-edit amend), outgoing/incoming (pre/post push review), and whatever project-specific deploy/sync workflow you run repeatedly. Start there, add incrementally.


References


Share this post on:

Previous Post
Rewriting History Precisely with git rebase -i and --onto
Next Post
Git Stash Beyond the Basics