Skip to content

Git Aliases: Faster, Clearer Git Commands

Lesson 7 of 11Intermediate9 min readModern Git Workflows · Modern Git ProductivityVerified: Git 2.43.0 on Ubuntu 24.04; every alias in this lesson was created and run

A Git alias is a configuration entry that expands to another command. git st runs git status; git lg runs whatever twelve-flag log invocation you actually want.

Aliases are stored in Git configuration, so they are not shell aliases and they work regardless of which shell you use. They are the cheapest productivity improvement in this cluster, and the easiest to overdo.

Terminal window
git config --global alias.st status

What it doesWrites an alias.st entry into your global Git configuration, so git st becomes shorthand for git status.

Why we run itAliases live in Git config rather than your shell, so they work identically in bash, zsh, PowerShell and any tool that shells out to Git.

Expected resultNo output. git st now behaves exactly as git status would.

Aliases with arguments work the same way — quote the whole value:

Terminal window
git config --global alias.lg "log --oneline --graph --decorate -20"
Terminal window
git lg
* 2588332 (HEAD -> main) api change 5
* 3b0d2f7 api change 4
* b146290 api change 3

Any arguments you pass are appended, so git lg --author=alice works as expected.

Aliases are ordinary configuration, which means the usual scopes apply:

ScopeCommandFileApplies to
Global--global~/.gitconfigAll your repositories
Local--local.git/configOne repository
System--system/etc/gitconfigEveryone on the machine

Global is almost always right. A local alias is occasionally useful for a project-specific command, but remember that it is not shared — .git/config is not committed.

Editing the file directly is often easier than repeated git config calls:

[alias]
st = status --short --branch
lg = log --oneline --graph --decorate -20
last = log -1 --stat

An alias whose value does not begin with ! is a Git subcommand plus arguments. Git prepends git and appends whatever you typed.

[alias]
co = checkout
sw = switch
br = branch
st = status --short --branch
ci = commit
unstage = restore --staged
last = log -1 --stat
amend = commit --amend --no-edit

These are safe, portable and readable. If you only ever create simple aliases, you will get most of the benefit with none of the pitfalls.

Prefix the value with ! and Git runs it through the shell instead:

Terminal window
git config --global alias.root '!pwd'
git root
/home/you/project

This unlocks anything a shell can do — pipes, multiple commands, other programs — with two important caveats.

A common surprise: shell aliases append arguments rather than substituting them.

Terminal window
git config --global alias.bad '!git log --since=$1'
git bad "1 week ago"

$1 is empty, and "1 week ago" is appended to the end of the command. The usual fix is to define and immediately call a shell function:

Terminal window
git config --global alias.since '!f() { git log --oneline --since="$1"; }; f'
git since "1 year ago"
2588332 api change 5
3b0d2f7 api change 4

The trailing ; f calls the function; without it, nothing runs. Arguments are still appended after f, which is exactly what makes "$1" work.

A shell alias executes from the top level of the working tree, not from your current directory. That is usually convenient and occasionally surprising if you expected a relative path to resolve where you stood.

These are chosen to be useful and to remain comprehensible to someone reading over your shoulder.

[alias]
st = status --short --branch
last = log -1 --stat
lg = log --oneline --graph --decorate -20
graph = log --oneline --graph --decorate --all
who = shortlog --summary --numbered --no-merges
[alias]
unstage = restore --staged
amend = commit --amend --no-edit
wip = commit --no-verify -m "wip"
[alias]
branches = branch --sort=-committerdate --format='%(committerdate:relative)%09%(refname:short)'
merged = branch --merged
unmerged = branch --no-merged

branches is the one people keep — it lists branches newest first with their age, which is exactly the view you want when deciding what to delete:

3 minutes ago main
2 days ago feature/search-ranking
5 weeks ago spike/new-parser
[alias]
review = "!f() { git diff \"${1:-main}...HEAD\"; }; f"
incoming = "!f() { git log --oneline \"HEAD..${1:-origin/main}\"; }; f"
outgoing = "!f() { git log --oneline \"${1:-origin/main}..HEAD\"; }; f"

review uses the three-dot form, comparing against the merge base rather than the branch tip — which is what you want when reading your own branch. Git Branches Explained covers why.

[alias]
undo = reset --soft HEAD~1
rl = reflog --date=relative

undo removes the last commit while keeping every change staged — the safe form. It deliberately does not use --hard.

Some aliases make destructive operations too easy.

# Do not do this
[alias]
nuke = reset --hard
f = push --force
cleanup = "!git reset --hard && git clean -fdx"

The problem is not that these commands exist — it is that shortening them removes the pause that made you think. git push --force is long enough to prompt a moment’s consideration; git f is not.

Similarly, avoid aliasing anything that combines reset --hard with clean -fdx. That pair deletes uncommitted changes and untracked files including ignored ones — configuration, environment files, build caches. It is occasionally what you want, and it should always be typed out.

Terminal window
git config --global alias.status "log -1"
git status --short
?? .githooks/

The built-in wins. Git resolves real commands before aliases, so you cannot accidentally break status, commit or push.

This is a safety feature. It also means an alias that appears not to work may be colliding with a subcommand you did not realise existed — check with git help -a.

List them:

Terminal window
git config --global --get-regexp '^alias\.'
alias.st status
alias.lg log --oneline --graph --decorate -5
alias.root !pwd
alias.since !f() { git log --oneline --since="$1"; }; f

See what one expands to:

Terminal window
git config --get alias.lg
log --oneline --graph --decorate -5

Remove one:

Terminal window
git config --global --unset alias.st

Find out where an alias came from, when the same name exists in several scopes:

Terminal window
git config --show-scope --show-origin --get alias.st

Aliases are personal configuration and are not shared by cloning. That is mostly a good thing — people have different preferences — but it has consequences worth planning for.

Documentation should use real commands. A README saying “run git lg” is useless to anyone without your config. Write the full command.

Scripts must not use aliases. A CI job or shell script invoking git st breaks on any machine where that alias does not exist. Always spell commands out in automation.

Shared aliases need a setup step. A project that genuinely wants shared aliases can commit a config file and have people include it:

Terminal window
git config --local include.path ../.gitaliases

The path is relative to .git/, so ../.gitaliases refers to a file at the repository root. That file can contain an [alias] section like any other config. The include.path line itself still has to be run per clone.

An alias can invoke another, because expansion happens at run time:

[alias]
lg = log --oneline --graph --decorate -20
lga = "!git lg --all"

Note that lga must be a shell alias. A simple alias expands to a Git subcommand, and lg is not one — Git resolves built-in commands and then aliases, but a simple alias’s first word must be a real command. Prefixing with ! sidesteps this by going through the shell, where git lg resolves normally.

This works and is occasionally useful. It also makes debugging harder: a broken alias three levels deep produces an error mentioning none of the names you typed. Keep chains to one level.

A few aliases that solve real problems rather than just shortening commands.

Fixup the commit that last touched a file — the tedious part of the autosquash workflow is finding the target commit:

[alias]
fixup = "!f() { git commit --fixup=\"$(git log -n1 --format=%h -- \"$1\")\"; }; f"

git fixup src/parser.py creates a fixup commit targeting whichever commit last modified that file.

Show what a branch adds, against the merge base:

[alias]
what = "!f() { git log --oneline \"${1:-main}..HEAD\"; }; f"

Find deleted files in history, which is otherwise an awkward invocation:

[alias]
deleted = "!f() { git log --diff-filter=D --name-only --format='%h %s'; }; f"

Open the repository’s default branch:

[alias]
main = "!git switch \"$(git symbolic-ref --short refs/remotes/origin/HEAD | sed 's|origin/||')\""

Useful on teams where some repositories use main and others master.

Expecting $1 to work without a function. Shell aliases append arguments. Wrap in f() { … }; f.

Forgetting the trailing ; f. The function is defined and never called, so nothing happens.

Aliasing destructive commands to short names. Removes the friction that prevents accidents.

Using aliases in scripts or documentation. They do not exist on other machines.

Cryptic names. An alias you cannot remember is worse than the command it replaced.

Trying to override a built-in. Git resolves real commands first.

Quoting errors. Check with git config --get alias.<name>; edit ~/.gitconfig directly for anything complex.

Building an enormous collection. Ten aliases you use daily beat sixty you half-remember.

An alias is a shorter name for something you already do.

It does not add capability, and it should not add risk. The best ones replace a command you type several times a day with something you can type without thinking; the worst ones make a dangerous command easy to type without thinking.

  • Aliases live in Git configuration, so they work in any shell and any tool that invokes Git.
  • A value not starting with ! is a Git subcommand; ! runs it through the shell.
  • Arguments are appended, not substituted — positional arguments need f() { … }; f.
  • Aliases cannot override built-in commands.
  • git config --get-regexp '^alias\.' lists them; --show-origin finds where one came from.
  • They are personal configuration and are not cloned; never use them in scripts or documentation.
  • Anything needing to change directory must be a shell function, not a Git alias.
  • Do not shorten destructive commands; the length is part of the safety.
  1. Create a simple alias and use it:

    Terminal window
    git config --global alias.st "status --short --branch"
    git st
  2. One with arguments:

    Terminal window
    git config --global alias.lg "log --oneline --graph --decorate -10"
    git lg
    git lg --author="$(git config user.name)"

    Note that your extra argument was appended.

  3. A shell alias:

    Terminal window
    git config --global alias.root '!pwd'
    cd some/subdirectory && git root

    Predict: which directory does it print?

  4. The positional-argument trap:

    Terminal window
    git config --global alias.bad '!git log --oneline --since=$1'
    git bad "1 week ago"

    Observe that it does not do what you meant.

  5. Fix it with a function:

    Terminal window
    git config --global alias.since '!f() { git log --oneline --since="$1"; }; f'
    git since "1 week ago"
  6. Try to shadow a built-in:

    Terminal window
    git config --global alias.status "log -1"
    git status --short

    Predict the result.

  7. Clean up:

    Terminal window
    git config --global --unset alias.status
    git config --global --unset alias.bad
    git config --global --get-regexp '^alias\.'

Steps 4 and 6 are the two behaviours that surprise people: arguments append rather than substitute, and built-ins always win.

Maintenance is the counterpart to ergonomics — keeping the repository itself fast as it grows.