Version Control

Custom Git Commands and Aliases

A practical guide to creating Git aliases, custom commands, shell functions, and workflow automation scripts that accelerate daily Git operations.

Custom Git Commands and Aliases

The default Git commands are verbose. git status, git log --oneline --graph --decorate, git diff --staged — you type these dozens of times a day. Aliases reduce them to git s, git l, git ds. Custom commands go further, combining multiple operations into single commands that match your workflow.

I have accumulated aliases over ten years. Every time I type the same sequence of commands three times, it becomes an alias. The result is a Git interface that feels like it was designed for my specific workflow.

Prerequisites

  • Git installed (v2.20+)
  • A shell (bash, zsh, fish, or PowerShell)
  • Your .gitconfig file location: ~/.gitconfig
  • Terminal access

Git Aliases Basics

Setting Aliases

# Command line (writes to ~/.gitconfig)
git config --global alias.s "status --short --branch"
git config --global alias.l "log --oneline --graph --decorate -20"

# Or edit ~/.gitconfig directly
# ~/.gitconfig
[alias]
    s = status --short --branch
    l = log --oneline --graph --decorate -20
    d = diff
    ds = diff --staged
    co = checkout
    cb = checkout -b
    cm = commit -m
    ca = commit --amend --no-edit

Using Aliases

git s              # git status --short --branch
git l              # git log --oneline --graph --decorate -20
git ds             # git diff --staged
git cb new-feature # git checkout -b new-feature
git cm "Fix bug"   # git commit -m "Fix bug"

Essential Aliases

Status and Diff

[alias]
    s = status --short --branch
    d = diff
    ds = diff --staged
    dw = diff --word-diff
    dt = difftool
    stat = diff --stat

Log

[alias]
    l = log --oneline --graph --decorate -20
    la = log --oneline --graph --decorate --all -30
    ll = log --pretty=format:'%C(yellow)%h%Creset %C(green)%ad%Creset %s %C(cyan)<%an>%Creset%C(red)%d%Creset' --date=short -20
    lf = log --pretty=format:'%C(yellow)%h%Creset %s' --name-only -10
    today = log --since='midnight' --oneline --author='Shane'
    week = log --since='1 week ago' --oneline --author='Shane'
    standup = log --since='yesterday' --oneline --author='Shane'

Branching

[alias]
    co = checkout
    cb = checkout -b
    br = branch
    bra = branch -a --sort=-committerdate
    brd = branch -d
    brD = branch -D
    branches = branch -a --sort=-committerdate --format='%(color:yellow)%(refname:short)%(color:reset) %(color:green)%(committerdate:relative)%(color:reset) %(subject)'
    merged = branch --merged main
    unmerged = branch --no-merged main

Committing

[alias]
    cm = commit -m
    ca = commit --amend --no-edit
    cam = commit --amend -m
    wip = commit -am "WIP"
    undo = reset --soft HEAD~1
    unstage = reset HEAD --

Stashing

[alias]
    sl = stash list
    sp = stash pop
    sa = stash apply
    ss = stash push -m
    sd = stash drop

Remote Operations

[alias]
    f = fetch --all --prune
    pl = pull --rebase
    ps = push
    psu = push -u origin HEAD
    psf = push --force-with-lease

Shell Command Aliases

Aliases starting with ! run shell commands:

[alias]
    # Open the repository in the browser
    browse = !git remote get-url origin | sed 's/git@/https:\\/\\//' | sed 's/\\.git$//' | sed 's/com:/com\\//' | xargs open

    # Show the root directory of the repo
    root = !pwd

    # Count commits by author
    who = !git shortlog -sn --no-merges

    # Show files changed in the last commit
    last = show --stat HEAD

    # Delete all merged branches
    cleanup = !git branch --merged main | grep -v main | xargs -r git branch -d

    # Quick amend and add all
    oops = !git add -A && git commit --amend --no-edit

    # Show the current branch name
    current = rev-parse --abbrev-ref HEAD

    # Create a save point
    save = !git tag save/$(date +%Y%m%d-%H%M%S)

Shell Functions in Aliases

For complex logic, use shell functions:

[alias]
    # Interactive rebase last N commits
    ri = "!f() { git rebase -i HEAD~${1:-5}; }; f"

    # Create and checkout branch with prefix
    feature = "!f() { git checkout -b feature/$1; }; f"
    bugfix = "!f() { git checkout -b bugfix/$1; }; f"
    hotfix = "!f() { git checkout -b hotfix/$1; }; f"

    # Find a commit by message
    find = "!f() { git log --all --oneline --grep=\"$1\"; }; f"

    # Find commits that changed a file
    history = "!f() { git log --oneline --follow -- \"$1\"; }; f"

    # Show diff between current branch and main
    review = "!f() { git diff main...HEAD; }; f"

    # Publish current branch
    pub = "!f() { git push -u origin $(git rev-parse --abbrev-ref HEAD); }; f"

    # Sync branch with main
    sync = "!f() { git fetch origin && git rebase origin/main; }; f"

    # Done with branch — merge to main and delete
    done = "!f() { \
        local branch=$(git rev-parse --abbrev-ref HEAD); \
        git checkout main && \
        git pull origin main && \
        git merge $branch && \
        git branch -d $branch; \
    }; f"

Usage:

git ri 10           # Interactive rebase last 10 commits
git feature search  # Creates feature/search branch
git find "payment"  # Find commits mentioning "payment"
git history app.js  # Show all commits that changed app.js
git pub             # Push current branch with -u
git sync            # Rebase onto latest origin/main
git done            # Merge current branch to main, delete it

Custom Git Commands

Any executable named git-<name> on your PATH becomes a Git command:

# Create ~/bin/git-standup
#!/bin/bash
# Show what you worked on recently

AUTHOR="${1:-$(git config user.name)}"
SINCE="${2:-yesterday}"

echo "Commits by $AUTHOR since $SINCE:"
echo ""
git log --all --oneline --author="$AUTHOR" --since="$SINCE" --no-merges
chmod +x ~/bin/git-standup

# Now use it as a git command
git standup
git standup "Shane" "3 days ago"

Useful Custom Commands

git-recent — Show recently worked branches:

#!/bin/bash
# ~/bin/git-recent

git for-each-ref --sort=-committerdate refs/heads/ \
    --format='%(color:yellow)%(refname:short)%(color:reset) %(color:green)(%(committerdate:relative))%(color:reset) %(subject)' \
    --count="${1:-10}"

git-changelog — Generate changelog between tags:

#!/bin/bash
# ~/bin/git-changelog

FROM="${1:-$(git describe --tags --abbrev=0 2>/dev/null || echo '')}"
TO="${2:-HEAD}"

if [ -z "$FROM" ]; then
    echo "No tags found. Showing all commits."
    git log --oneline --no-merges
    exit 0
fi

echo "Changes from $FROM to $TO:"
echo ""

echo "### Features"
git log "$FROM".."$TO" --oneline --no-merges --grep="^feat:" | sed 's/^[a-f0-9]* feat: /- /'

echo ""
echo "### Bug Fixes"
git log "$FROM".."$TO" --oneline --no-merges --grep="^fix:" | sed 's/^[a-f0-9]* fix: /- /'

echo ""
echo "### Other"
git log "$FROM".."$TO" --oneline --no-merges --grep="^feat:" --grep="^fix:" --invert-grep | sed 's/^[a-f0-9]* /- /'

git-stats — Repository statistics:

#!/bin/bash
# ~/bin/git-stats

echo "=== Repository Statistics ==="
echo ""
echo "Commits: $(git rev-list --count HEAD)"
echo "Contributors: $(git shortlog -sn --no-merges | wc -l)"
echo "Files: $(git ls-files | wc -l)"
echo "Branches: $(git branch | wc -l)"
echo "Tags: $(git tag | wc -l)"
echo ""
echo "Size: $(git count-objects -vH | grep size-pack | awk '{print $2, $3}')"
echo ""
echo "Top 5 contributors:"
git shortlog -sn --no-merges | head -5
echo ""
echo "Most changed files (last 100 commits):"
git log --pretty=format: --name-only -100 | sort | uniq -c | sort -rn | head -10

git-ignore — Add patterns to .gitignore:

#!/bin/bash
# ~/bin/git-ignore

if [ -z "$1" ]; then
    echo "Usage: git ignore <pattern>"
    echo "       git ignore node_modules"
    echo "       git ignore '*.log'"
    exit 1
fi

echo "$1" >> .gitignore
echo "Added '$1' to .gitignore"
git add .gitignore

Complete Working Example: Full Git Configuration

# ~/.gitconfig

[user]
    name = Shane
    email = [email protected]

[core]
    editor = code --wait
    autocrlf = input
    pager = less -FRX
    excludesfile = ~/.gitignore_global

[init]
    defaultBranch = main

[pull]
    rebase = true

[push]
    default = current
    autoSetupRemote = true

[merge]
    conflictStyle = zdiff3
    tool = vscode

[mergetool "vscode"]
    cmd = code --wait --merge $REMOTE $LOCAL $BASE $MERGED

[diff]
    algorithm = histogram
    colorMoved = default

[rerere]
    enabled = true

[fetch]
    prune = true
    writeCommitGraph = true

[branch]
    sort = -committerdate

[alias]
    # === Status & Diff ===
    s = status --short --branch
    d = diff
    ds = diff --staged
    dw = diff --word-diff

    # === Log ===
    l = log --oneline --graph --decorate -20
    la = log --oneline --graph --decorate --all -30
    ll = log --pretty=format:'%C(yellow)%h%Creset %C(green)%ad%Creset %s %C(cyan)<%an>%Creset%C(red)%d%Creset' --date=short -20
    today = log --since='midnight' --oneline
    week = log --since='1 week ago' --oneline

    # === Branching ===
    co = checkout
    cb = checkout -b
    branches = branch -a --sort=-committerdate --format='%(color:yellow)%(refname:short)%(color:reset) %(color:green)(%(committerdate:relative))%(color:reset) %(subject)'
    merged = branch --merged main
    unmerged = branch --no-merged main

    # === Committing ===
    cm = commit -m
    ca = commit --amend --no-edit
    cam = commit --amend -m
    wip = !git add -A && git commit -m "WIP"
    undo = reset --soft HEAD~1
    unstage = reset HEAD --

    # === Remote ===
    f = fetch --all --prune
    pl = pull --rebase
    pub = "!f() { git push -u origin $(git rev-parse --abbrev-ref HEAD); }; f"
    psf = push --force-with-lease

    # === Stash ===
    sl = stash list
    sp = stash pop
    ss = stash push -m

    # === Workflow ===
    feature = "!f() { git checkout -b feature/$1; }; f"
    bugfix = "!f() { git checkout -b bugfix/$1; }; f"
    sync = "!f() { git fetch origin && git rebase origin/main; }; f"
    done = "!f() { local b=$(git rev-parse --abbrev-ref HEAD); git checkout main && git pull && git merge $b && git branch -d $b; }; f"
    cleanup = !git branch --merged main | grep -v main | xargs -r git branch -d
    ri = "!f() { git rebase -i HEAD~${1:-5}; }; f"

    # === Info ===
    last = show --stat HEAD
    who = shortlog -sn --no-merges
    find = "!f() { git log --all --oneline --grep=\"$1\"; }; f"
    history = "!f() { git log --oneline --follow -- \"$1\"; }; f"
    current = rev-parse --abbrev-ref HEAD
    root = rev-parse --show-toplevel

    # === Safety ===
    save = "!f() { git tag save/$(date +%Y%m%d-%H%M%S); }; f"
    oops = !git add -A && git commit --amend --no-edit

Common Issues and Troubleshooting

Alias conflicts with existing Git commands

You cannot override built-in Git commands with aliases:

Fix: Git ignores aliases that match built-in command names. Use different names. For example, git ci instead of trying to alias git commit.

Shell alias with special characters fails

Quotes, pipes, and dollar signs need careful escaping in .gitconfig:

Fix: Use double quotes around the alias value and escape internal quotes. Or use !f() { ...; }; f function syntax for complex shell commands. Test with git config --get alias.name to verify the stored value.

Alias works in terminal but not in scripts

Shell aliases (starting with !) use /bin/sh, which may not support all bash features:

Fix: Explicitly call bash in the alias: !bash -c '...'. Or write a standalone script and put it on PATH as git-commandname.

Custom command not found

The git-name script is not on PATH or is not executable:

Fix: Ensure the script directory is in your PATH. Add export PATH="$HOME/bin:$PATH" to your shell profile. Make the script executable: chmod +x ~/bin/git-name.

Best Practices

  • Start with the top 10 aliases. s, l, d, ds, co, cb, cm, ca, f, pl cover most daily operations. Add more as patterns emerge.
  • Use consistent naming conventions. Two-letter aliases for single commands (co, cm). Descriptive words for compound operations (sync, cleanup, done).
  • Avoid aliasing destructive operations. Keep dangerous commands explicit. git push --force should not become git pf — the extra typing is a safety feature.
  • Use --force-with-lease instead of --force. If you alias force push, always use --force-with-lease to prevent overwriting someone else's work.
  • Put custom commands in ~/bin/ and add them to PATH. This keeps them organized and accessible. Name them git-name so they work as git name.
  • Document your aliases in comments. Future you will not remember what git ri 10 does. Add comments in .gitconfig.
  • Share aliases with your team. Create a documented .gitconfig template for the team. Common aliases reduce communication friction.
  • Test aliases before relying on them. Run the expanded command manually first to verify it does what you expect. Then alias it.

References

Powered by Contentful