Software Engineering Wiki

Practice

Git

Inspecting history, undoing changes safely, resolving conflicts and the object model that explains why each command behaves as it does.

Cheatsheet #

TaskCommand
What is going ongit status -sb
Staged vs working treegit diff (unstaged), git diff --staged
Compact historygit log --oneline --graph --decorate --all
Who changed this linegit blame -L 10,20 file
Unstage a filegit restore --staged file
Discard local changesgit restore file
Undo last commit, keep changesgit reset --soft HEAD~1
Undo a pushed commitgit revert <sha>
Amend the last commitgit commit --amend --no-edit
Park workgit stash push -m wip / git stash pop
Recover anythinggit reflog
File from another branchgit restore --source main -- path
Search all history for textgit log -S 'needle' --oneline
Rebase onto latest maingit fetch && git rebase origin/main
Branches already mergedgit branch --merged main
Delete remote branchgit push origin --delete feature-x
Second working treegit worktree add ../hotfix hotfix
Who owns the most churngit shortlog -sn --no-merges

The model in one paragraph #

Git stores snapshots as immutable objects: blobs (content), trees (directories), commits (a tree plus parents plus metadata). A branch is a movable pointer to a commit, HEAD points at the current branch, and the index is a staging area between the working tree and the next commit. Every command that “changes history” writes new objects and moves a pointer — the old commits stay reachable through the reflog until garbage collection, which is why almost nothing is truly lost.

git cat-file -p HEAD          # the commit object: tree, parent, author
git cat-file -p HEAD^{tree}   # its directory listing
git rev-parse HEAD            # resolve a ref to a SHA

Inspecting #

git status -sb                                        # short status plus tracking info
git log --oneline --graph --decorate --all            # topology at a glance
git log --since='2 weeks' --author=jodis --stat
git log -p -- path/to/file                            # history of one file, with diffs
git log --follow -- path/to/file                      # ...across renames
git log -S 'AWS_SECRET' --oneline                     # commits that add or remove a string
git log -G 'regex' --oneline                          # commits whose diff matches a regex
git log --grep 'JIRA-123' --oneline                   # commits whose message matches
git log main..feature --oneline                       # in feature, not in main
git log --left-right --oneline main...feature         # divergence on both sides
git show <sha> --stat
git blame -L 20,40 -- file
git diff main...feature                               # changes since the branches diverged

A..B means “reachable from B but not A”; A...B in log means the symmetric difference, but in diff it means “changes on B since it diverged from A”. The inconsistency is historical and worth memorising.

Undoing #

Pick the command by what you need to preserve.

GoalCommandSafe on a pushed branch
Unstage, keep the editgit restore --staged fileYes
Throw away local editsgit restore fileYes, but the edit is gone
Undo commit, keep stagedgit reset --soft HEAD~1No
Undo commit, keep working treegit reset HEAD~1No
Undo commit and the changesgit reset --hard HEAD~1No, and the work is gone
Reverse a commit with a new onegit revert <sha>Yes — this is the one for shared branches
Fix the last commit’s messagegit commit --amendNo
Restore a deleted filegit restore --source HEAD~1 -- pathYes
git restore --source main -- config/app.yaml    # one file from another branch
git revert -m 1 <merge-sha>                     # revert a merge, keeping mainline 1
git reset --hard origin/main                    # make local match remote exactly
git clean -nd                                   # preview untracked removal
git clean -fd                                   # remove untracked files and directories

reset --hard and clean -fd delete work that was never committed

The reflog can recover commits, not uncommitted edits. Run git stash push -u first if there is any doubt.

Reflog: the undo history #

Every move of HEAD is recorded locally for 90 days by default, including resets, rebases and checkouts.

git reflog                                # HEAD movements with reasons
git reflog show feature-x                 # one branch's movements
git reset --hard HEAD@{2}                 # back to where you were two moves ago
git branch rescue <sha-from-reflog>       # recover a deleted branch
git fsck --lost-found                     # dangling commits, when even reflog was pruned

Branches #

git switch -c feature-x                   # create and switch (modern spelling)
git switch -                              # previous branch
git switch --detach <sha>                 # look around without moving a branch
git branch -vv                            # local branches and their upstreams
git branch --merged main                  # already in main: safe to delete
git branch --no-merged main               # still carrying unique commits
git push -u origin feature-x              # push and set upstream
git push origin --delete feature-x
git fetch --prune                         # drop remote-tracking refs that no longer exist
# Delete every local branch already merged into main
git branch --merged main | grep -vE '^\*|main|master' | xargs -r git branch -d

Rebase and merge #

Merge preserves what actually happened; rebase produces a history that reads as if the work were done in order. Rebase your own unpublished branch, merge everything shared.

git fetch origin
git rebase origin/main                    # replay my commits on top of main
git rebase -i origin/main                 # squash, reword, drop, reorder
git rebase --continue | --skip | --abort
git merge --no-ff feature-x               # keep the branch's shape in history
git merge --squash feature-x              # one commit, no merge record
git pull --rebase                         # avoid merge commits from pulling
git config --global pull.rebase true

git push --force-with-lease instead of --force: it refuses when the remote moved since your last fetch, so it cannot silently overwrite a colleague’s push.

Conflicts #

A conflict is two commits changing the same region. Git stages what it could resolve and leaves the rest marked.

git status                                # "both modified" lists the conflicts
git diff --diff-filter=U                  # only unresolved files
git checkout --ours -- file               # keep the current branch's version
git checkout --theirs -- file             # keep the incoming version
git add file && git rebase --continue
git merge --abort                         # back to before the merge
git rerere status                         # replay a previously recorded resolution
git config --global rerere.enabled true

During a rebase “ours” is the branch being replayed onto (upstream) and “theirs” is your commit, which is the reverse of a merge. Read git status rather than trusting the words.

Stash and worktrees #

git stash push -u -m 'wip: auth refactor'  # -u includes untracked files
git stash list
git stash show -p stash@{0}
git stash pop                              # apply and drop
git stash apply stash@{1}                  # apply and keep
git stash branch fix-auth stash@{0}        # turn a stash into a branch

git worktree add ../hotfix hotfix          # second checkout, same repository
git worktree list
git worktree remove ../hotfix

Worktrees beat stashing for “I need to look at another branch right now”: no context switch, no risk of popping into the wrong tree.

Remotes and authentication #

git remote -v
git remote set-url origin git@github.com:example/repo.git
git config --global credential.helper 'cache --timeout=3600'
git config --global url."git@github.com:".insteadOf "https://github.com/"
git clone --filter=blob:none <url>        # partial clone: history without old blobs
git clone --depth 1 <url>                 # shallow, for CI
git fetch --unshallow                     # turn a shallow clone into a full one

Store credentials in a helper, never in the remote URL: a token in origin leaks through git remote -v, CI logs and shell history.

Tags #

git tag -a v1.4.2 -m 'release 1.4.2'      # annotated: has an author, date and message
git tag -l 'v1.*' --sort=-v:refname
git push origin v1.4.2
git push origin --tags
git tag -d v1.4.2 && git push origin :refs/tags/v1.4.2
git describe --tags --always              # human-readable version from the nearest tag

Lightweight tags (git tag v1) are a bare pointer with no metadata — fine for scratch marks, wrong for releases.

Bisect #

git bisect start
git bisect bad                             # current commit is broken
git bisect good v1.4.0                     # this one was fine
# ...test, then mark each step
git bisect run ./test.sh                   # or automate: exit 0 = good, non-zero = bad
git bisect reset

git bisect run with a script that reproduces the bug turns “somewhere in 300 commits” into eight or nine automatic steps.

Hooks and configuration #

git config --global user.name 'Jodis Fields'
git config --global user.email 'you@example.com'
git config --global commit.gpgsign true
git config --global init.defaultBranch main
git config --global core.excludesFile ~/.gitignore
git config --global rebase.autosquash true
git config --list --show-origin           # which file set what
# .git/hooks/pre-commit — reject obvious secrets
#!/usr/bin/env bash
if git diff --cached -U0 | grep -nE 'AKIA[0-9A-Z]{16}|BEGIN (RSA|OPENSSH) PRIVATE KEY'; then
  echo 'possible credential in staged changes' >&2
  exit 1
fi

Hooks live in .git/hooks and are not cloned. Set core.hooksPath to a tracked directory to share them.

Oneliners #

# Branches by last commit date, newest first
git for-each-ref --sort=-committerdate refs/heads --format='%(committerdate:short) %(refname:short) %(authorname)'

# Commits per author, excluding merges
git shortlog -sn --no-merges

# Files changed most often
git log --format=format: --name-only | sort | uniq -c | sort -rn | head

# Largest objects in the repository
git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | awk '$1=="blob"' | sort -k3 -nr | head

# What landed between two tags
git log --oneline --no-merges v1.4.0..v1.5.0

# Everything that touched a directory last month
git log --since='1 month' --oneline -- infra/

# Which branches contain a commit
git branch -a --contains <sha>

# The commit that introduced a string
git log -S 'deprecatedFlag' --oneline --reverse | head -1

# Diff ignoring whitespace churn
git diff -w --ignore-blank-lines

# Show a file as it was at a date
git show 'HEAD@{2024-01-15}:path/to/file'

# Apply one commit from another branch
git cherry-pick -x <sha>

# Stage only part of a file
git add -p file

# Rewrite author on the last commit after fixing config
git commit --amend --reset-author --no-edit

# Repository size and object count
git count-objects -vH

# Prune and repack a bloated clone
git gc --prune=now --aggressive

Last updated 15 September 2026 · Edit this page