This document covers Git commands and features that are powerful but less frequently used in Day-to-day workflows. These tools solve specific problems around metadata, history manipulation, Repository integrity, and multi-tree management.
Power tools for Git experts: Advanced Git commands are like power tools in a workshop — you do not need them every day, but when you do, they save hours of manual work. Commands like git bisect, git stash, and git worktree solve specific problems that basic Git cannot handle efficiently.
Why it matters: These commands solve real-world problems — git bisect finds the exact commit that introduced a bug in minutes instead of hours, git stash lets you context-switch without losing work, and git worktree lets you work on multiple branches simultaneously.
The key insight: git reflog is your safety net — it records every movement of HEAD, so even if you accidentally reset or rebase, you can always recover your previous state.
git replace allows you to tell Git to use one object in place of another without rewriting the Object database. When Git encounters the original object, it transparently substitutes the Replacement. This is a non-destructive mechanism for altering how history appears.
Definition. A replacement ref is a reference stored under refs/replace/ that maps an original Object hash to a replacement object hash. Git resolves these references during object lookups, Presenting the replacement as if it were the original.
## Replace one object with another
git replace <original-object> <replacement-object>
## Replace using an edited version of the original
git replace --edit <object>
# Graft: make a commit appear to have a different parent
git replace --graft <commit> [<parent>...]
# Delete a specific replacement
# Delete all replacements
git replace -d $( git replace -l )
When you run git replace A BGit creates a ref at refs/replace/A pointing to B. During any Object lookup, Git checks whether the requested object has an entry under refs/replace/. If it Does, Git returns the replacement instead.
a3f2b1c0... -> d4e5f6a7... (commit replacement)
b7c8d9e0... -> e1f2a3b4... (blob replacement)
This means:
The original object still exists in the object store, unchanged. The replacement object must already exist in the object store. The replacement is local by default; it is not pushed unless you explicitly push refs/replace/. # List all replacement refs
# Show what an object is replaced with
$ git cat-file -p $( git replace -l | head -1 )
# Show a replaced commit as it appears after replacement
$ git log --no-replace-objects -1 <original-commit>
$ git log -1 <original-commit>
The --no-replace-objects flag disables replacement resolution, letting you see the raw original Object.
Grafting re-parents a commit, making it appear as if it has different parents. This is useful for Stitching together unrelated histories.
# Make commit C appear as if root-commit is its parent (joining two histories)
git replace --graft <commit-C> <root-commit>
# Make a commit appear as a root commit (no parents)
git replace --graft <commit>
# Make a commit appear to have two parents (octopus merge)
git replace --graft <commit> <parent1> <parent2>
`refs/replace/`. They differ from the older `~/.git/info/grafts` mechanism, which was not ref-based And could not be pushed or shared.# Create an edited replacement for a commit
git replace --edit <commit>
# This opens your editor with the commit"s contents.
# You can modify the commit message, author, committer, or parent list.
# Git creates a new commit object and registers it as the replacement.
Use cases for --edit:
Fixing a typo in a commit message deep in history. Correcting author email or name. Changing the commit message to reference an issue tracker. Suppose commit abc1234 has a bad message, but it is 50 commits deep with many branches depending On it. An interactive rebase would be disruptive. Instead:
# Create a new commit with the same tree but different message
git commit-tree abc1234^{tree} -p abc1234^ -m " Correct commit message "
# Register the replacement
git replace abc1234 def5678
Now git log shows def5678 with the corrected message, but the original abc1234 remains in the Object store. All child commits still reference abc1234 internally, but Git transparently shows def5678.
You can replace two adjacent commits with a single squashed commit:
# Create a new commit with the combined changes
git merge-tree --write-tree $( git hash-file -t commit first^ ) first second
# Then create the commit object
git commit-tree <tree-hash> -p second^ -m " Combined: first and second "
# Edit the commit to fix the author
git replace --edit <commit>
# In the editor, change the author line
Replacements are transient by default. To bake them into the object store permanently:
# Rewrite history to make replacements permanent
git filter-branch --all -- --no-replace-objects
# Or with git-filter-repo (preferred)
git filter-repo --replace-refs delete-no-add
History. If the replacement changes commit hashes, downstream branches may break. Coordinate with Your team before pushing replacement refs.Operation Mechanism Permanence Pushes by default git replaceRef under refs/replace/ Transient (local) No git replace --graftReplacement commit ref Transient (local) No ~/.git/info/graftsGrafts file (legacy) Local only No git rebaseNew commit objects Permanent Yes git filter-branchRewrites object store Permanent Yes
git notes attaches arbitrary metadata to Git objects ( commits) without modifying the Objects themselves. Notes are stored as ordinary Git objects in a special ref, making them versioned And distributable.
Definition. A Git note is a blob object associated with a specific commit, stored in a tree Under refs/notes/commits. The mapping from commit to note is maintained through a tree structure Where each path component corresponds to characters of the commit hash.
git notes add [-m " message " | -F <file>] [<object>]
# Show the note for a commit
git notes show [<object>]
# Edit the note for a commit (opens editor)
git notes edit [<object>]
git notes remove [<object>]
# Copy notes from one commit to another
git notes copy <from-object> <to-object>
# Append to an existing note
git notes append [-m " message " ] [<object>]
# Merge notes from another ref
git notes merge <notes-ref>
Notes live under refs/notes/commits. The tree structure maps commit hashes to note blobs using a Directory tree keyed by hex characters of the commit hash:
refs/notes/commits -> tree
c3d4e5f6... -> blob (note content for commit abc3d4e5f6...)
f7a8b9c0... -> blob (note content for commit def7a8b9c0...)
This structure enables efficient lookups without scanning all notes.
# Add a note to the current HEAD
$ git notes add -m " Reviewed by Sarah, approved with minor nits "
[notes/commits abc1234] Notes added to HEAD
# Add a note to a specific commit
$ git notes add -m " Build #4521 failed: test_timeout " a3f2b1c0
Reviewed by Sarah, approved with minor nits
# List all notes (shows commit hash and note blob hash)
a3f2b1c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6 b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6
# Edit a note in your configured editor
# Remove a note from a specific commit
$ git notes remove a3f2b1c0
# Append additional information to an existing note
$ git notes append -m " Follow-up: regression confirmed in v2.3.1 "
If no note exists for the commit, append behaves like add.
When notes from different branches diverge, you can merge them:
# Merge notes from another notes ref
$ git notes merge refs/notes/commits-from-other-branch
# Merge with a specific strategy
$ git notes merge -s ours refs/notes/commits-from-other-branch
$ git notes merge -s theirs refs/notes/commits-from-other-branch
$ git notes merge -s cat-sort-uniq refs/notes/commits-from-other-branch
Available merge strategies:
Strategy Behavior oursDiscard incoming notes, keep local theirsDiscard local notes, take incoming manualLeave conflicts for manual resolution cat-sort-uniqConcatenate, sort lines, remove duplicates
You can maintain multiple sets of notes using different refs:
# Create a notes ref for CI results
$ git notes --ref=refs/notes/ci add -m " Build passed, all 847 tests green " HEAD
$ git notes --ref=refs/notes/ci show HEAD
# Create a notes ref for code review
$ git notes --ref=refs/notes/reviews add -m " LGTM " HEAD
# List notes in a specific namespace
$ git notes --ref=refs/notes/reviews list
By default, git log does not display notes. Use --notes to include them:
# Show notes alongside commit messages
# Show notes from a specific namespace
$ git log --notes=refs/notes/ci
# Show notes from all namespaces
# Show notes with a custom format
$ git log --format= " %h %s%n%N " --notes
The %N format specifier expands to the note content. %n expands to a newline.
Notes refs are not pushed or fetched by default:
$ git push origin refs/notes/commits
$ git fetch origin refs/notes/commits:refs/notes/commits
$ git push origin ' refs/notes/*:refs/notes/* '
# Configure a remote to push notes automatically
$ git config remote.origin.push refs/notes/ * :refs/notes/ *
# After reviewing a commit, attach the review summary
$ git notes add -m " Code review: 3 issues found
- Missing null check on line 42
- Use enum instead of int for status field
- Consider extracting helper function " a3f2b1c0
# In a CI pipeline, attach build results to the commit
$ git notes --ref=refs/notes/ci add -m " CI Result: SUCCESS
# Attach issue tracker references to commits
$ git notes add -m " Jira: PROJ-1234
# Add rationale for non-obvious changes
$ git notes add -m " Why this approach:
The naive solution would iterate O(n^2), but we can reduce this
to O(n log n) by using a balanced BST. Benchmarked on 10M records:
Access to `refs/notes/commits` can modify notes. Do not rely on notes for security-critical Metadata.git describe produces a human-readable name for a commit based on the nearest annotated tag. It is Primarily used for generating version strings in build systems.
Definition. git describe finds the most recent annotated tag that is an ancestor of the given Commit, then appends the number of additional commits and an abbreviated object name to uniquely Identify the commit.
# Describe the current HEAD
# Describe a specific commit
# Describe using all tags (not just annotated ones)
# Describe using all refs (branches, tags, etc.)
# Set the minimum abbreviation length
git describe --abbrev=<n>
# Use the longest match instead of the nearest tag
# Always show the full abbreviated hash (equivalent to --abbrev=40)
# Mark the working tree as dirty if there are uncommitted changes
# Append a suffix for dirty trees
git describe --dirty=-modified
# Exclude certain patterns from matching
git describe --match=<pattern>
# Only match tags that contain the pattern
# Find the tag the commit is pointed at by
Breakdown:
Component Meaning v2.3.1The nearest annotated tag that is an ancestor of the commit -1414 commits after the tag -gLiteral “g” (for “git”) a3f2b1cAbbreviated commit hash
If the commit itself is tagged, git describe outputs just the tag name:
Flag Effect --tagsUse lightweight tags in addition to annotated tags --allUse any ref, not just tags --abbrev=<n>Use at least n hex digits (default: 7, or auto-detected) --abbrev=0Show only the tag name, no commit count or hash --longAlways show the commit count and abbreviated hash --alwaysFall back to abbreviated hash if no tag is found --dirtyAppend -dirty if working tree has modifications --dirty=<suffix>Append custom suffix instead of -dirty --match=<pattern>Only consider tags matching the glob pattern --exclude=<pattern>Exclude tags matching the glob pattern --containsFind the tag that contains the commit (tag is a descendant) --first-parentFollow only the first parent when traversing
git describe is ideal for embedding version information in binaries:
VERSION := $( shell git describe --dirty --always --tags )
CFLAGS := -DVERSION=\" $( VERSION ) \"
return subprocess.check_output(
[ " git " , " describe " , " --dirty " , " --always " , " --tags " ],
stderr = subprocess. DEVNULL
except subprocess.CalledProcessError:
__version__ = get_version()
VERSION = $( git describe --dirty --always --tags 2> /dev/null || echo " unknown " )
echo " Building version: $VERSION "
# Working tree has uncommitted changes
# Only show tag, no suffix
$ git describe --abbrev=0
# Use any ref, not just tags
# Find tag containing this commit (useful for "which release is this in?")
$ git describe --contains a3f2b1c
# Match only release tags
$ git describe --match ' v[0-9]* '
# Exclude pre-release tags
$ git describe --exclude ' *-rc* ' --exclude ' *-beta* '
`git tag` without `-a` or `-s`) are ignored unless you pass `--tags`. This is a deliberate Design choice: annotated tags carry metadata (tagger, date, message) that makes them suitable for Release identification..gitattributes is a configuration file that assigns attributes to paths in a repository. It Controls how Git handles specific files: line ending conversion, diff generation, merge behavior, Binary detection, and more.
Definition. A .gitattributes file maps path patterns to attribute lists. Each line has the Format pattern attribute1 attribute2=value. Git evaluates .gitattributes files hierarchically: The one in the repository root, then those in subdirectories, with more specific paths taking Precedence.
.gitattributes # Root: applies to all paths
.gitattributes # src/: applies to paths under src/
.gitattributes # src/lib/: most specific, wins for paths under src/lib/
Git also reads from:
Location Scope .gitattributes in repo rootAll files in the repo .gitattributes in subdirectoriesFiles in that subtree $GIT_DIR/info/attributesLocal repo, not committed ~/.gitattributesUser-global, all repos /etc/gitattributesSystem-wide
# Comment lines start with #
# Blank lines are ignored
# Patterns use glob syntax
vendor/* linguist-generated
docs/_build/* linguist-generated
*.haml linguist-language=Ruby
*.c whitespace=blank-at-eol
*.py whitespace=trailing-space
Attributes can be set, unset, or set to a value:
Syntax Meaning textSet the attribute -textUnset the attribute text (after !text)Unset the attribute (explicitly) diff=markdownSet attribute to value markdown
The text attribute controls CRLF/LF conversion:
Setting Behavior textConvert CRLF to LF on commit; convert to OS-native on checkout text eol=lfConvert CRLF to LF on commit; LF on checkout (force Unix line endings) text eol=crlfConvert LF to CRLF on checkout (force Windows line endings) -textNo conversion at all (binary-like) binaryEquivalent to -text -diff
# Force LF for everything (recommended for cross-platform projects)
# Windows-specific files keep CRLF
# Shell scripts always use LF
(not binary). Using `* text=auto eol=lf` in the root `.gitattributes` is the recommended practice For cross-platform projects. It normalizes committed files to LF while letting Windows developers Check out with CRLF if their `core.autocrlf` is set.# Mark files as binary (disables diff and merge)
# The `binary` attribute is a shorthand for:
You can define custom diff drivers for specific file types:
# Configure a diff driver in .gitconfig
$ git config diff.markdown.textconv pandoc --from=markdown --to=plain
$ git config diff.protobuf.textconv protoc --decode_raw
# Use the custom diff driver
$ git diff # Will use pandoc for .md files, protoc for .proto files
# Define a merge driver in .gitconfig
$ git config merge.json.name " JSON merge driver "
$ git config merge.json.driver " json-merge-tool %O %A %B %L "
$ git config merge.json.recursive binary
Available built-in merge strategies:
Driver Behavior merge=oursKeep our version, ignore theirs entirely merge=binaryNo merging; conflict on any modification
The export-subst attribute enables keyword expansion in git archive output:
# Enable export-subst for the version file
$ git archive --format=tar.gz HEAD > release.tar.gz
# VERSION file in the archive will have expanded values:
# Date: 2026-04-07 10:30:00 +0000
Available format specifiers:
Specifier Expands to %HFull commit hash %hAbbreviated commit hash %DRef names (tags, branches) %ciCommit date in ISO 8601 format %anAuthor name %aeAuthor email %cnCommitter name %ceCommitter email %sSubject (first line of commit message) %bBody (rest of commit message)
# Detect trailing whitespace errors
*.c whitespace=trailing-space
*.py whitespace=blank-at-eol
# Detect space-before-tab
*.java whitespace=space-before-tab
# Detect indentation with spaces for files that use tabs
Makefile whitespace=indent-with-non-tab
Whitespace Attribute Error Detected trailing-spaceTrailing whitespace, blank lines with whitespace space-before-tabSpaces before tab characters indent-with-non-tabIndentation using spaces when tab width is expected cr-at-eolCarriage return at end of line (not an error, just detection) blank-at-eolTrailing whitespace at end of line blank-at-eofBlank lines at end of file
# Tell GitHub Linguist not to count these for language stats
vendor/* linguist-generated
node_modules/* linguist-generated
*_pb2.py linguist-generated
*.pb.go linguist-generated
docs/_build/* linguist-generated
# Override detected language
*.haml linguist-language=Ruby
*.cubescript linguist-language=C
# Check which attributes apply to a file
$ git check-attr -a -- src/main.c
# Check a specific attribute
$ git check-attr text -- README.md
# Check what binary detection says
$ git check-attr binary -- image.png
Line endings. After adding or modifying `.gitattributes`You must re-normalize existing files:# Renormalize all files according to new .gitattributes
$ git add --renormalize .
$ git commit -m " Normalize line endings per .gitattributes "
The .gitmodules file records the configuration for submodules in a repository. It is a plain-text INI-style file that maps submodule names to their paths and URLs.
url = https://github.com/nlohmann/json.git
[submodule "libs/catch2"]
url = https://github.com/catchorg/Catch2.git
git submodule add <url> <path>
git submodule add -b <branch> <url> <path>
git submodule add --depth 1 <url> <path>
# Initialize and clone submodules (after git clone)
git submodule update --init
git submodule update --init --recursive
# Update all submodules to latest remote state
git submodule update --remote
# Update a specific submodule
git submodule update --remote libs/json
git submodule foreach --recursive ' echo $path '
By default, submodules track a specific commit (detached HEAD). You can configure a tracking branch:
# Add with a tracking branch
git submodule add -b main https://github.com/org/repo.git libs/repo
# Configure an existing submodule to track a branch
git config -f .gitmodules submodule.libs/repo.branch main
git submodule update --remote libs/repo
Branch name. The tracking branch tells `git submodule update --remote` which branch to fetch From.# Add a shallow submodule (single commit)
git submodule add --depth 1 <url> <path>
# Configure an existing submodule as shallow
git config -f .gitmodules submodule.libs/repo.shallow true
# 1. Deinitialize the submodule
git submodule deinit -f libs/repo
# 2. Remove the submodule from Git tracking
# 3. Remove the submodule's data
rm -rf .git/modules/libs/repo
git commit -m " Remove libs/repo submodule "
configuration in `.gitmodules` and `.git/modules/`. This causes errors for anyone cloning the Repository. Always follow the full removal procedure.Pitfall Solution Submodule directory empty after clone Run git submodule update --init --recursive Submodule shows “modified” after git pull Submodules track commits, not branches; run git submodule update --init --recursive Forgot to commit .gitmodules changes Stage .gitmodules and the submodule path before committing Stale submodule in .git/modules/ after removal Manually remove .git/modules/<name> Submodule at wrong commit cd libs/repo && git checkout <commit> then cd ../.. && git add libs/repo
git bundle creates a single file that contains a packfile along with header information about the Refs it contains. Bundles can be transported via any medium (USB, email, HTTP) and then cloned or Fetched from as if they were a remote.
Definition. A Git bundle is a self-contained binary file encoding a Git packfile and a ref Index. It represents a slice of repository history defined by a set of prerequisites (commits that Must already exist) and a set of included refs.
# Bundle the entire repository
git bundle create repo.bundle --all
# Bundle a specific branch
git bundle create feature.bundle main
# Bundle a range of commits
git bundle create changes.bundle origin/main..HEAD
# Bundle with specific tags
git bundle create release.bundle --all --tags
# Bundle since a specific commit
git bundle create incremental.bundle a3f2b1c0..HEAD
# Verify a bundle can be applied to the current repository
$ git bundle verify repo.bundle
The bundle contains 1 ref
The bundle requires these 0 ref ( s ) to satisfy prerequisites
# Verify a bundle with prerequisites
$ git bundle verify changes.bundle
The bundle contains 1 ref
The bundle requires these 1 ref ( s ) to satisfy prerequisites
If prerequisites are missing, git bundle verify reports which commits are needed.
# Clone from a bundle (creates a new repository)
git clone repo.bundle my-repo
# The cloned repository has the bundle configured as a remote
origin /path/to/repo.bundle (fetch)
origin /path/to/repo.bundle (push)
# Fetch refs from a bundle into an existing repository
git fetch repo.bundle refs/heads/feature:refs/heads/feature
# Unbundle (extract without updating refs)
git bundle unbundle repo.bundle
For large repositories, you can create incremental bundles that only contain new commits:
# First bundle: everything
git bundle create full.bundle --all
# Second bundle: only new commits since the first bundle
git bundle create incr1.bundle origin/main..main
# Third bundle: only new commits since the second bundle
git bundle create incr2.bundle incr1.bundle..main
# Machine A (with internet): create bundle
$ git bundle create project.bundle --all
# Transfer via USB, SCP, or sneakernet
$ scp project.bundle user@offline-machine:/tmp/
# Machine B (offline): clone from bundle
$ git clone /tmp/project.bundle project
In security-sensitive environments where machines have no network access:
# On the build server (has internet access)
$ git bundle create deps.bundle --all
$ cp deps.bundle /media/usb/
# On the air-gapped machine
$ git clone /media/usb/deps.bundle source
$ git fetch /media/usb/incremental.bundle
# In CI: store the build commit as a bundle artifact
$ git bundle create build- $CI_BUILD_ID .bundle HEAD
# Later: verify the exact state that produced the build
$ git bundle verify build-1234.bundle
And tags, always use `--all`. If you need to include unreachable objects (e.g., dangling commits), Use `git bundle create repo.bundle --all --reflog`.git worktree manages multiple working directories linked to the same repository. Each worktree can Be checked out to a different branch, enabling parallel work without stashing or committing Incomplete changes.
Definition. A linked worktree is a separate directory tree that shares the same object database, Refs, and configuration as the main repository. Each worktree has its own working directory, index (staging area), and HEADBut all write operations go to the same .git directory.
git worktree add <path> <branch>
git worktree add -b <new-branch> <path> <start-point>
git worktree add --detach <path> <start-point>
git worktree remove <path>
# Prune stale worktree admin files
# Lock a worktree (prevent removal)
git worktree lock --reason " active hotfix " <path>
git worktree unlock <path>
# Create a worktree for an existing branch
$ git worktree add ../hotfix-worktree hotfix/urgent-fix
Preparing worktree (checking out ' hotfix/urgent-fix ' )
HEAD is now at a3f2b1c Fix critical auth bypass
# Create a worktree with a new branch
$ git worktree add -b feature/oauth2 ../oauth2-worktree main
# Create a detached worktree (for inspecting a commit)
$ git worktree add --detach ../inspect-worktree a3f2b1c
.git/ # Full .git directory
../hotfix-worktree/ # Linked worktree
.git # File (not directory) pointing to repo/.git/worktrees/hotfix-worktree
The .git file in the linked worktree contains:
gitdir: /path/to/repo/.git/worktrees/hotfix-worktree
Internally, the main repository maintains worktree metadata:
commondir # Points to main .git
gitdir # Points back to linked worktree
/path/to/repo a3f2b1c0 [main]
/path/to/hotfix-worktree b7c8d9e0 [hotfix/urgent-fix]
/path/to/oauth2-worktree def56789 [feature/oauth2]
# Remove a worktree (must have a clean working directory)
$ git worktree remove ../hotfix-worktree
# Force remove (discards uncommitted changes)
$ git worktree remove --force ../hotfix-worktree
# Prune admin files for deleted worktrees
Worktrees are particularly powerful with bare repositories. You can create a bare repo as a central Hub and check out working directories from it:
# Create a bare repository
$ git clone --bare https://github.com/org/project.git project.git
# Create worktrees from the bare repo
$ git -C project.git worktree add ../project-main main
$ git -C project.git worktree add ../project-dev develop
# Now you have two working directories from the same bare repo
# You are working on a feature, but need to switch to a hotfix
# Instead of stashing your feature work:
$ git worktree add ../hotfix hotfix/critical-bug
# Fix the bug, commit, push
# Your feature work is untouched
# Create a worktree to review someone's PR without disturbing your work
$ git worktree add --detach ../review origin/pr/42
# Run tests, inspect code
$ git worktree remove ../review
# Build from multiple branches simultaneously
$ git worktree add ../build-v2.3 v2.3
$ git worktree add ../build-v2.4 v2.4
$ make -C ../build-v2.3 release
$ make -C ../build-v2.4 release
Constraint Details Branch can only be checked out in one worktree Git prevents checking out the same branch in multiple worktrees No nested worktrees You cannot create a worktree inside another worktree core.bare must be unset for main worktreeBare repos can only have linked worktrees, not a main worktree git init and git clone create the main worktreeYou cannot convert a standalone repo into a linked worktree
`git worktree remove`Git leaves stale administrative files. Run `git worktree prune` to clean them Up. The branch that was checked out in the deleted worktree may remain locked until you prune.The reflog records every movement of branch tips and HEAD. It is Git’s primary recovery mechanism For operations that seem destructive, such as git reset --hard``git rebaseOr git commit --amend.
# Show a specific ref's reflog
# Show reflog for a branch
# Limit the number of entries
# Show reflog with relative dates
git reflog --date=relative
a3f2b1c0 HEAD@{0}: commit: Add user authentication module
b7c8d9e0 HEAD@{1}: rebase -i (finish): returning to refs/heads/feature
c1d2e3f4 HEAD@{2}: rebase -i (pick): Refactor database queries
d4e5f6a7 HEAD@{3}: rebase -i (start): checkout HEAD~5
a3f2b1c0 HEAD@{4}: checkout: moving from main to feature
b7c8d9e0 HEAD@{5}: commit: Fix login redirect loop
e1f2a3b4 HEAD@{6}: checkout: moving from hotfix to main
You can address any commit using HEAD@{n} or branch@{n}:
# Checkout the commit HEAD was at 3 moves ago
# Reset to the state main was at 5 moves ago
$ git reset --hard main@{ 5 }
# Cherry-pick a commit from the reflog
$ git cherry-pick HEAD@{ 2 }
# Show the diff between current state and 5 moves ago
# Scenario: you ran `git reset --hard HEAD~3` and lost 3 commits
a3f2b1c0 HEAD@{ 0 }: reset: moving to HEAD~3
d4e5f6a7 HEAD@{ 1 }: commit: Third lost commit
c1d2e3f4 HEAD@{ 2 }: commit: Second lost commit
b7c8d9e0 HEAD@{ 3 }: commit: First lost commit
# Recover: reset back to before the destructive operation
$ git reset --hard HEAD@{ 1 }
# Or: create a new branch at the lost state
$ git branch recovered-work HEAD@{ 1 }
# Scenario: interactive rebase went wrong, commits are rearranged or dropped
e1f2a3b4 HEAD@{ 0 }: rebase -i (finish): returning to refs/heads/feature
a3f2b1c0 HEAD@{ 1 }: rebase -i (pick): Rewritten commit
b7c8d9e0 HEAD@{ 2 }: rebase -i (start): checkout HEAD~3
b7c8d9e0 HEAD@{ 3 }: commit: Original commit 1
c1d2e3f4 HEAD@{ 4 }: commit: Original commit 2
d4e5f6a7 HEAD@{ 5 }: commit: Original commit 3
# Abort the rebase by resetting to before it started
$ git reset --hard HEAD@{ 3 }
The reflog has a default expiry period. After expiry, entries are removed and the objects they Reference become eligible for garbage collection.
# Default expiry: 90 days for reachable entries, 30 days for unreachable
$ git config gc.reflogExpire
$ git config gc.reflogExpireUnreachable
# Increase the expiry period
$ git config gc.reflogExpire 180.days
$ git config gc.reflogExpireUnreachable 90.days
# Disable reflog expiry entirely (reflog entries kept forever)
$ git config gc.reflogExpire never
$ git config gc.reflogExpireUnreachable never
# Expire reflog entries immediately (DANGEROUS)
$ git reflog expire --expire=now --all
# Expire reflog entries older than a specific date
$ git reflog expire --expire=2026-01-01 --all
Collection from reclaiming objects referenced only by the reflog. Over time, this can significantly Increase repository size. For large repositories, consider a reasonable expiry period (e.g., 365 Days) instead.git gc runs git reflog expire as part of its process. Objects that are only reachable through Expired reflog entries become eligible for removal.
# Run garbage collection (also expires old reflog entries)
# Run gc without expiring reflog
# Dry run: see what would be collected
$ git gc --prune=now --dry-run
git fsck (filesystem check) verifies the integrity and connectivity of the Git object database. It Detects corrupt objects, dangling references, and other repository health issues.
# Full check including all objects
# Check connectivity (reachable from refs)
git fsck --connectivity-only
# Check a specific object
# Check with verbose output
# Do not consider reflogs as reachable
Check Description Missing objects References point to objects that do not exist Corrupt objects Object content does not match its hash Dangling blobs Blobs not reachable from any tree Dangling trees Trees not reachable from any commit Dangling commits Commits not reachable from any ref Unreachable objects Objects not reachable from any ref or reflog Invalid tree entries Tree entries with invalid mode or filename Invalid parent links Commits referencing non-existent parent commits Tag signature Verification of signed tags (with --tag)
dangling blob 3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2
dangling commit a3f2b1c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6
dangling tree b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6
Dangling objects are not necessarily a problem. They are created by normal operations:
git add then git reset creates a dangling blob.Deleted branches leave dangling commits. Aborted rebases leave dangling commits and trees. $ git cat-file -p 3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2
$ git log --oneline a3f2b1c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6
# Recover a dangling commit as a branch
$ git branch recovered a3f2b1c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6
# Remove all dangling objects (run gc after verifying you do not need them)
# Check for corrupt objects
$ git fsck --full 2>&1 | grep -i " corrupt\|error\|missing "
error: object a3f2b1c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6: corrupted pack file
# Identify which pack file contains the corrupt object
$ git verify-pack -v .git/objects/pack/ * .idx 2>&1 | grep a3f2b1c0
# Re-fetch the corrupt object from the remote
# Or: if the remote does not have it, restore from a backup
# Step 1: Identify the problem
# Step 2: If a pack file is corrupt, remove it and re-fetch
$ rm .git/objects/pack/corrupt-file.pack
$ rm .git/objects/pack/corrupt-file.idx
# Step 3: If individual objects are corrupt
$ git fsck --full 2>&1 | grep " corrupt\|missing " | while read _ _ hash ; do
echo " Attempting to recover $hash "
git cat-file -t " $hash " 2> /dev/null || echo " Object $hash is unrecoverable "
# Step 4: If recovery is impossible, remove the corrupt ref
$ git update-ref -d refs/heads/broken-branch
Methodically: identify, back up, then repair. If the `.git` directory itself is corrupted (e.g., From disk failure), restore from backup before attempting Git-level repairs.git rerere (Reuse Recorded Resolution) remembers how you resolved merge conflicts and Automatically applies the same resolution when the same conflict arises again. This is particularly Valuable for long-lived feature branches that repeatedly merge from the main branch.
$ git config --global rerere.enabled true
$ git config rerere.enabled true
# Also auto-update the index with the recorded resolution
$ git config rerere.autoupdate true
A merge conflict occurs. You resolve it manually. rerere records the pre-conflict state (ours/theirs/base) and your resolution in .git/rr-cache/.The next time the same files conflict with the same base content, rerere detects the match and applies the recorded resolution automatically. # Check the current status of rerere
# Show recorded resolutions
# Run rerere manually (after resolving conflicts)
a3f2b1c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6/
preimage # The conflict markers before resolution
postimage # The resolved content
thisimage # The current state (during conflict)
# First occurrence: merge conflict in src/parser.c
CONFLICT (content): Merge conflict in src/parser.c
# Resolve the conflict in your editor
# rerere records the resolution automatically
$ git commit -m " Merge main into feature-branch "
# ... time passes, main has new commits ...
# Second occurrence: same conflict arises again
CONFLICT (content): Merge conflict in src/parser.c
# rerere has already resolved this for you
Auto-resolving conflict in src/parser.c using recorded resolution
$ git commit -m " Merge main into feature-branch "
If a feature branch lives for weeks and you merge main into it regularly, the same conflicts tend To recur. rerere eliminates the need to resolve them repeatedly.
If you maintain a set of patches (e.g., vendor patches) that you rebase periodically, rerere Remembers how to resolve the recurring conflicts.
If multiple developers encounter the same conflict, share the .git/rr-cache/ directory to Propagate resolutions:
# Copy rerere cache to another clone
$ cp -r .git/rr-cache/ /path/to/other-clone/.git/rr-cache/
Option Default Description rerere.enabledfalseEnable rerere rerere.autoupdatefalseAutomatically stage the recorded resolution rerere.autogctrueRun git gc on rr-cache when it gets large
Conflict context changes even slightly, `rerere` will not match and you will need to resolve Manually. The resolution is then recorded for future use.git format-patch generates patch files from commits in a format suitable for email-based code Review. git am (apply mailbox) applies those patches, recreating the original commits with their Metadata (author, date, message).
# Create a patch for the last commit
# Create patches for the last 3 commits
# Create patches for all commits on a branch since divergence from main
$ git format-patch main..HEAD
# Create patches for a specific range
$ git format-patch a3f2b1c0..b7c8d9e0
# Output patches to a specific directory
$ git format-patch -o /tmp/patches main..HEAD
# Create patches with a different subject prefix
$ git format-patch --subject-prefix= " PATCH v2 " main..HEAD
# Create a single patch containing all commits (diffstat summary)
$ git format-patch --cover-letter -1
# Number patches starting from a specific number
$ git format-patch -3 -v 2
The output of git format-patch is an email-compatible format:
From a3f2b1c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6 Mon Sep 17 00:00:00 2001
From: Jane Developer <jane@example.com>
Date: Mon, 7 Apr 2026 10:30:00 +0000
Subject: [PATCH 1/3] Add authentication middleware
This commit adds JWT-based authentication middleware to the API
gateway. The middleware validates tokens on every request and
populates the request context with user information.
Signed-off-by: Jane Developer <jane@example.com>
<!-- Breadcrumb Schema for SEO -->
<script type="application/ld+json">
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [{"name": "Home", "url": "https://wyattau.com"}, {"name": "tools", "url": "https://tools.wyattau.com"}, {"name": "Git", "url": "https://tools.wyattau.com/git"}, {"name": "Advanced Git Commands", "url": "https://tools.wyattau.com/git/advanced-git-commands"}]
src/middleware/auth.go | 87 +++++++++++++++++++++++++++++++
tests/auth_test.go | 45 +++++++++++++++
2 files changed, 132 insertions(+)
create mode 100644 src/middleware/auth.go
create mode 100644 tests/auth_test.go
diff --git a/src/middleware/auth.go b/src/middleware/auth.go
+++ b/src/middleware/auth.go
$ git am /tmp/0001-Add-authentication-middleware.patch
# Apply all patches in a directory (in order)
$ git am /tmp/patches/ * .patch
# Apply a patch from stdin (mbox format)
$ git am < /tmp/patch-series.mbox
# Abort a failed application
# Continue after resolving conflicts
# Apply patches without committing (just stage)
$ git am --3way /tmp/0001- * .patch
$ git am --signoff /tmp/patches/ * .patch
# Apply and keep subject line intact
$ git am --keep-cr /tmp/patches/ * .patch
# Generate a cover letter template along with patches
$ git format-patch --cover-letter -3
# This creates 0000-cover-letter.patch (empty, for you to fill in)
# and 0001-*.patch, 0002-*.patch, 0003-*.patch
The cover letter template:
From a3f2b1c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6 Mon Sep 17 00:00:00 2001
From: Jane Developer <jane@example.com>
Date: Mon, 7 Apr 2026 10:30:00 +0000
Subject: [PATCH 0/3] *** SUBJECT HERE ***
Add authentication middleware
src/middleware/auth.go | 87 +++++++++++++++++++++++++
src/middleware/rate.go | 56 +++++++++++++++
src/middleware/logging.go | 43 +++++++++++
3 files changed, 186 insertions(+)
# Create patches from your branch
$ git format-patch -o patches/ main..HEAD
# Send for review, receive feedback, make changes
# Recreate patches with updated version number
$ git format-patch -o patches/ -v 2 main..HEAD
# When patches are accepted, apply them to the target branch
# Apply with 3-way merge (safer)
$ git am --3way patches/0001- * .patch
# Fix conflicts in the files listed
$ git add <resolved-files>
# If the patch is fundamentally wrong:
Information (not author) will be different. If you need to preserve exact commit hashes, use `git cherry-pick` or `git merge` instead.git send-email sends patches created by git format-patch as emails. This is the primary code Contribution workflow for the Linux kernel and many other open-source projects.
You need a working email sending setup. Common transports:
$ git config sendemail.smtpEncryption tls
$ git config sendemail.smtpServer smtp.gmail.com
$ git config sendemail.smtpServerPort 587
$ git config sendemail.smtpUser your-email@gmail.com
$ git config sendemail.smtpPass your-app-password
$ git config sendemail.sendmailcmd /usr/sbin/sendmail
$ git config sendemail.from " Your Name <your-email@example.com> "
### Basic Usage
# Send the last commit as a patch
$ git format-patch -1 --stdout | git send-email --to maintainer@example.com \
--cc reviewer@example.com
# Send patches from files
$ git send-email --to maintainer@example.com 0001- * .patch 0002- * .patch 0003- * .patch
# Send all patches in a directory
$ git send-email --to maintainer@example.com --to-list patches/
# Send with a cover letter
$ git format-patch --cover-letter -5
$ git send-email --to maintainer@example.com 0000- * .patch 0001- * .patch
# Dry run (print what would be sent without sending)
$ git send-email --to maintainer@example.com --dry-run 0001- * .patch
# Send with In-Reply-To (for threading)
$ git send-email --to maintainer@example.com --in-reply-to= " <message-id@example.com> " 0001- * .patch
$ git send-email --cc reviewer1@example.com --cc reviewer2@example.com 0001- * .patch
# Cc everyone who authored the patches
$ git send-email --cc-cmd " ./scripts/get-maintainer.pl " 0001- * .patch
# Set the In-Reply-To header for proper threading
$ git send-email --in-reply-to= " <20260407103000.12345@example.com> " 0001- * .patch
# Suppress Cc from the patch body (Signed-off-by, etc.)
$ git send-email --suppress-cc=cc 0001- * .patch
# Suppress Cc from body, sob, and misc-cmds
$ git send-email --suppress-cc=body,sob,misc-cmd 0001- * .patch
Flag Suppresses Cc from --suppress-cc=authorPatch author --suppress-cc=sobSigned-off-by trailers --suppress-cc=ccCc lines in patch body --suppress-cc=bodyccCc, Acked-by, Reviewed-by trailers --suppress-cc=bodyEntire patch body --suppress-cc=misc-cmdOutput of —cc-cmd --suppress-cc=allAll of the above
$ git format-patch --cover-letter -5 -o outgoing/
0001-fix-memory-leak-in-parser.patch
0002-add-bounds-checking.patch
0003-refactor-tokenizer.patch
0005-update-documentation.patch
# 3. Edit the cover letter
$ vim outgoing/0000-cover-letter.patch
$ git send-email --to maintainer@project.org \
--dry-run outgoing/ * .patch
$ git send-email --to maintainer@project.org \
Config Key Description sendemail.fromDefault From address sendemail.toDefault To address sendemail.smtpServerSMTP server hostname sendemail.smtpServerPortSMTP server port sendemail.smtpEncryptionssl``tlsOr nonesendemail.smtpUserSMTP username sendemail.smtpPassSMTP password (or use credential helper) sendemail.chainReplyToChain emails as replies to cover letter sendemail.threadEnable threading sendemail.confirmauto``always``never``cc``compose
Accidental send to hundreds of subscribers is difficult to undo. Double-check recipient lists and Patch content before sending.Git uses -- to disambiguate between branch names and file paths. Without it, Git may misinterpret Arguments.
# Dangerous: if a file named "main" exists, this checks out the file, not the branch
# Correct: unambiguous checkout of the branch
# Dangerous: if a branch named "test.c" exists, this is ambiguous
# error: pathspec 'test.c' did not match any file(s) known to git
# Correct: explicitly disambiguate
$ git checkout -- test.c # Checkout the file
$ git checkout test.c -- # Treated as branch (but still ambiguous)
$ git switch test.c # Modern: explicitly switches to branch
# The -- separator in diff
$ git diff main -- src/file.c # Diff between branch "main" and file "src/file.c" in working tree
$ git diff -- main src/file.c # Diff between two commits/files (ambiguous without context)
`git restore` for files. These modern commands eliminate the ambiguity that `git checkout` suffers From.Git throws “ambiguous argument” errors when a name matches multiple objects:
# If both a tag and a branch are named "v2.0"
error: ambiguous argument ' v2.0 ' : both revision and filename
# Disambiguate using full ref notation
$ git checkout refs/heads/v2.0 # Branch
$ git checkout refs/tags/v2.0 # Tag
# Or use --detach for tags
$ git checkout v2.0^{commit} # The commit the tag points to
# Show all objects that match a prefix
Submodules track specific commits, not branches. This leads to common issues:
# After pulling the parent repo, submodule directories may be empty
# Submodule paths show as modified but you did not change them
modified: libs/json (new commits )
# The submodule is at a different commit than the parent records
$ cd libs/json && git log --oneline -3
# Shows commits that the parent does not know about
# Fix: update submodules to match the parent's recorded state
$ git submodule update --init --recursive
# Or: update the parent to track the submodule's current state
$ git commit -m " Update json submodule to latest "
Explicitly run `git submodule update --init --recursive` after pulling. Configure `submodule.recurse` to automate this:$ git config submodule.recurse true
This makes git pull``git checkoutAnd git switch also update submodules recursively.
Each branch can only be checked out in one worktree at a time:
$ git worktree add ../wt2 main
fatal: " main'' is already checked out at " /path/to/repo '
# Solution: use a different branch
$ git worktree add -b main-fix ../wt2 main
$ git worktree add --detach ../wt2 main
If reflog entries expire before you recover lost commits, those commits are permanently Garbage-collected:
# Scenario: you ran a destructive operation 6 months ago
# Only shows the last 90 days (default)
# The lost commits may already be garbage-collected
# No dangling commits found
# Prevention: increase reflog expiry
$ git config gc.reflogExpire 365.days
$ git config gc.reflogExpireUnreachable 365.days
# Or: immediately create a branch for important states
$ git branch backup-before-rebase HEAD
Force pushing rewrites remote history, causing issues for collaborators:
# After rebasing, the remote branch history differs from local
$ git push origin feature
# error: failed to push some refs
# DANGEROUS: force push overwrites remote history
$ git push --force origin feature
# SAFER: force-with-lease only pushes if the remote has not changed
$ git push --force-with-lease origin feature
Command Behavior git push --forceUnconditionally overwrites the remote branch git push --force-with-leaseOnly overwrites if the remote ref matches your tracking branch git push --force-if-includesLike force-with-lease, but also checks that your local branch includes the remote tip
Your local history. Any collaborator who has based work on those commits will encounter conflicts. Always prefer `--force-with-lease` unless you are certain you are the only person working on the Branch.# Checking out a tag or specific commit puts you in detached HEAD
HEAD is now at a3f2b1c0 v2.3.1
# You are not on any branch
# Any commits made here will be lost when you switch branches
$ git commit -am " Hotfix on tag "
# Switching branches abandons this commit
# Solution: always create a branch first
$ git checkout -b hotfix/v2.3.1 v2.3.1
# Solution: if you already made commits, find them in the reflog
$ git branch hotfix/v2.3.1 HEAD@{ 1 }
# Accidentally staging everything instead of specific files
$ git add . # Stages ALL changes
# Use --patch for fine-grained staging
$ git add --patch src/file.c
# Interactively choose which hunks to stage
# Or use git restore to unstage
$ git restore --staged unwanted-file.c
Git cannot merge binary files. Any modification to a binary file results in a conflict:
CONFLICT (content): Merge conflict in image.png
$ git checkout --ours image.png # Keep our version
$ git checkout --theirs image.png # Keep their version
# Prevent binary conflicts with merge driver
The binary merge driver marks the file as unmergeable, and Git will keep whichever version Was last modified (based on the merge strategy). This avoids generating conflict markers in binary Files.
This topic covers the core concepts of advanced git commands, including underlying theory, practical implementation, and key applications.
Key concepts include:
Git fundamentals (add, commit, push, pull) branching and merging strategies resolving merge conflicts rebasing and cherry-picking Git workflows (GitFlow, trunk-based) Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Problem. Squash the last 4 commits into a single commit with a combined message.
Solution.
# Start interactive rebase for the last 4 commits
In the editor, change the last three pick lines to squash:
pick abc1234 First commit message
squash def5678 Second commit message
squash ghi9012 Third commit message
squash jkl3456 Fourth commit message
Git opens a second editor to combine the commit messages. Save the combined message.
# If already pushed, force-push to update the remote
git push --force-with-lease origin main
--force-with-lease is safer than --force because it rejects the push if someone else has pushed to the branch since your last pull.
■ \blacksquare ■
Problem. A critical bug fix was committed on the develop branch (commit a1b2c3d). Cherry-pick it into main without merging the entire branch.
Solution.
# Cherry-pick the specific commit
# If there are conflicts, resolve them and continue
git cherry-pick --continue
Cherry-pick creates a new commit on main with the same changes as a1b2c3d but a different commit hash. The original commit on develop is unaffected.
■ \blacksquare ■
git rebase -i rewrites history: reorder, squash, edit, or drop commits. Avoid on shared branches.git cherry-pick applies a specific commit from one branch onto another; creates a new commit hash.git replace substitutes one object for another without rewriting history; useful for grafting.git bisect uses binary search to find the commit that introduced a bug; O ( log n ) O(\log n) O ( log n ) steps.git stash temporarily shelves working directory changes; git stash pop restores them.