Git Operations Reference

A practical Git operations quick reference for real-world scenarios, helping you quickly find useful but hard-to-remember commands, and clarifying their usage conditions and execution impacts.

First published
Last updated

On this page

Basic Notations and Terminology

Notations in Command Formats

NotationMeaning
<name>Placeholder to be replaced with the actual value; angle brackets themselves are not entered
[<name>]Optional placeholder; square brackets themselves are not entered
<name>...One or more values of the same kind can be provided; ellipsis itself is not entered
A | BChoose one from A and B; the vertical bar itself is not entered

-ish

The English suffix -ish means “somewhat like” or “having the characteristics of”. Git documentation uses it to name a category of expressions that can be resolved to a specific object; it’s not a command parameter to be entered verbatim.

commit-ish means a name or expression that can ultimately be resolved to a commit, such as:

main          # Branch
v1.0.0        # Tag pointing to a commit
a1b2c3d       # commit ID
HEAD          # Current commit
HEAD~2        # The second-generation ancestor of the current commit
origin/main   # Remote-tracking branch

Therefore, [<commit-ish>] means you can fill in a branch, tag, commit ID, or HEAD expression here, or it can be omitted.

tree-ish means a name or expression that can ultimately be resolved to a tree object. Branches, commits, and tags pointing to commits can all be further resolved to the project file tree of that commit, so main, v1.0.0, HEAD, tree object IDs, and HEAD^{tree} can all serve as tree-ish.

HEAD is a special reference: under normal conditions, it points to the current branch; in a detached HEAD state, it points directly to a commit.

When the current branch is main, modifying files from a clean state, staging modifications, and creating commits will not change the HEAD → main reference relationship:

The four states from left to right are clean, modifications in working directory, modifications staged, and commit created. Each state explicitly shows HEAD pointing to main; the operations between states are modifying README.md, git add README.md, and git commit. clean Working Directory Modified Modifications Staged Commit Created HEAD main HEAD main HEAD main HEAD main modify README.md git add README.md git commit

If you directly switch to a certain commit ID, HEAD will no longer point to a branch, but directly to that commit. This is called detached HEAD, meaning HEAD is detached from branches:

HEAD → <commit-id>

You can still modify, stage, and commit under a detached HEAD state, but creating a commit will not move any local branch forward. To retain these commits, you should execute git switch -c <new-branch> to create a branch before leaving.

When executing an ordinary git commit, the new commit records the commit where the current branch was previously located; this commit immediately preceding the new commit is called its parent commit. The root commit at the beginning of history has no parent; an ordinary commit usually has one parent; and a commit resulting from merging multiple histories can have multiple parents. Both ^ and ~ leverage this relationship to navigate backward through commits.

Both start from a given commit, but their lookup rules differ:

  • <commit-ish>^<n>: Selects the n-th parent of <commit-ish>, where n starts from 1; when <n> is omitted, it selects the first parent.
  • <commit-ish>~<n>: Starting from <commit-ish>, consecutively selects the first parent n times; when <n> is omitted, it only selects once.

Therefore, ^2 means “the second parent commit,” while ~2 means “trace back two generations along the first parent chain”; <commit-ish>^, <commit-ish>^1, <commit-ish>~, and <commit-ish>~1 all represent the same commit.

For example, feature branches off from C1 on main. After executing git merge feature on main, a merge commit M is produced:

feature branches off from the C1 commit on main; main and feature advance to C2 and F2 respectively, then merge into M. HEAD, HEAD^, HEAD^2, and HEAD~2 are respectively marked around the corresponding commits. Execution Location: main Command Executed: git merge feature feature main C0 C1 C2 M F1 F2 HEAD^2 HEAD~2 HEAD^ HEAD

Each directed line in the figure points from a parent commit to the subsequent commit it produced.

First and second are not inferred based on positions or branch names in the figure, but rather on the order of multiple parent records within the merge commit object. When git merge feature is executed on main:

  1. Before the merge, HEAD points to main, and main points to C2; Git writes C2 into the first parent record.
  2. feature points to F2; during the merge, Git uses MERGE_HEAD to record this incoming commit, and writes F2 into the second parent record.

Therefore, the relevant content of the merge commit M is equivalent to:

parent <commit ID of C2>
parent <commit ID of F2>

The two directed lines in the figure pointing from C2 and F2 to M correspond to these two records. The HEAD^, HEAD^2, and HEAD~2 in the figure all assume that you remain on main after the merge is complete, at which point HEAD points to the merge commit M. Applying the aforementioned rules to HEAD at this time:

  • HEAD^: Takes the first parent of M, yielding C2;
  • HEAD^2: The number 2 means taking the second parent of M, yielding F2, not tracing back two generations;
  • HEAD~2: Takes the first parent consecutively twice, going through M → C2 → C1, yielding C1.

These notations start searching from the commit HEAD points to at the time they are used, and do not fixedly represent a certain commit in the figure. If you subsequently switch to feature, HEAD will point to F2, and at that point HEAD^ would represent F2’s first parent F1; the parent commit relationships of the merge commit M have not changed, it’s just no longer accessed using HEAD as the starting point.

If you instead executed git merge main on feature, HEAD before the merge would be F2, so the first record would be F2, and the second record would be the incoming C2.

Merge commits are not limited to just two parents. When merging multiple branches into main simultaneously, for example:

git merge feature-a feature-b

If this command successfully generates a multi-branch merge commit, its commit object can contain three parent records:

parent <commit ID where main was before merge>
parent <commit ID of feature-a>
parent <commit ID of feature-b>

This type of merge is called an Octopus merge. In this case, HEAD^3 reads the third parent record; it differs from HEAD~3, which would trace back three generations consecutively along the first parent record. Commits can record any number of parents, and the specific order can be viewed directly in the commit object:

git cat-file -p HEAD

Working Tree Status

The working tree (also often called the working directory) is the actual project files available for editing in the directory corresponding to the repository. Modifying files directly changes the working tree; newly created files that have not yet been tracked by Git will show up here as untracked files.

The index (also known as the staging area) stores the file snapshot information that will be used for the next commit; it is not a directory for people to edit directly. git add writes the current content of the specified files into the index, and git commit then creates a new commit based on the index.

The HEAD introduced earlier provides the file snapshot of the current commit. Based on the official Git git-status documentation, common statuses can be categorized into the following groups:

Common StatusWorking TreeIndexgit status Official Category
cleanMatches index for tracked files; no untracked files (except ignored ones)Matches HEADworking tree clean
Staged modificationsMay match index, or might be modified again after stagingSaves modifications to be included in next commit, differs from HEADChanges to be committed
Unstaged modificationsTracked files contain modifications not yet written to indexHasn’t saved latest working tree content, may also save earlier staged contentChanges not staged for commit
Untracked filesNew paths not yet tracked by Git existNo record of the pathUntracked files
Unmerged pathsConflict resolution not yet completeMay hold multiple pending versions of the same pathUnmerged paths

Except for clean, other statuses can appear simultaneously. For example, if a file is modified again after being staged, it will have both staged modifications and unstaged modifications simultaneously.

switch: Switch Branches

git switch <target-branch> attempts to make the current worktree use the target branch. The process for handling existing local states during the switch is as follows:

The current worktree switches from branch-a to branch-b. If staged modifications, unstaged modifications, and ordinary untracked files can be safely preserved, the switch succeeds, HEAD changes to point to branch-b, local states are preserved, and compared anew relative to branch-b; otherwise, the switch is aborted, and HEAD, index, and working directory remain unchanged. Local State: Staged modifications, unstaged modifications, and untracked files Before Switch HEAD → branch-a Index: Local state Working Tree: Local state Can local state be safely preserved? git switch branch-b Yes No Switch Succeeds HEAD → branch-b Local State: Preserved git status: Compared vs branch-b Switch Aborted HEAD → branch-a Index, Worktree: Unchanged Git reports blocking paths

restore: Unstage

Command-line parameters used in this section:

ParameterMeaning
-S, --stagedSets the restoration target to the index
# Purpose: Unstage modifications for specified paths
# Result: Restores the corresponding content in the index to HEAD's version, keeping the working directory files unchanged
git restore --staged <pathspec>...
# Example 1: Unstage README.md, but keep the file modifications
git restore --staged README.md

rebase: Replay Commits on a New Base

rebase transplants a commit sequence onto a new base. When executing git rebase <upstream>, Git finds the commits in the current branch that are not in <upstream>, recreating these commits sequentially after <upstream>, and finally points the current branch to the rebuilt sequence; <upstream> itself does not move.

Before rebasing, feature/example branched off from commit B on main and contains C, D; main has advanced to E. After executing git rebase main, main still points to E, the modifications of feature/example are recreated as C′, D′ after E, and HEAD still points to feature/example. Before Rebase After Rebase git rebase main A B E C D A B E C′ D′ main feature/example · HEAD main feature/example · HEAD

C' and D' are newly created commits based on the modifications of C and D. Because their parent commits have changed, they have new commit IDs. Rebasing is suitable for tidying up local history that has not yet been shared; if other branches or users are already working based on these commits, you should coordinate before rewriting them.

Command-line parameters used in this section:

ParameterMeaning
--onto <new-base>Changes the starting point of the rebuilt commits to <new-base>
-i, --interactiveEdit commit order and handling methods before rebuilding
--autostashTemporarily saves local modifications before rebasing, then reapplies them afterward
-r, --rebase-mergesAttempts to recreate original merge structure on the new base
--continueContinues the rebase after resolving conflicts or finishing edits
--skipSkips the commit currently being applied
--abortAborts the rebase and restores the branch, index, and tracked working directory content to before the start
# Purpose: Reapply commits unique to the current branch onto a new base
# Conditions: The current branch is the one to be rewritten; index and tracked files have no uncommitted modifications; related commits have not been shared or rewrite has been coordinated
# Result: Rebuilds current branch's commits after <upstream>, and points current branch to the new commit sequence; <upstream> remains unchanged
git rebase <upstream>
# Example 1: Current branch is feature/example, reapply its unique commits after main
git rebase main

# Purpose: Separately specify new base, commit selection boundary, and branch to rewrite
# Conditions: Index and tracked files have no uncommitted modifications; <branch> is not used by other worktrees; related commits have not been shared or rewrite coordinated
# Result: Selects commits unique to <branch> relative to <upstream>, reapplies them onto <new-base>, then points <branch> to the new sequence
git rebase --onto <new-base> <upstream> <branch>
# Example 1: Reapply commits from topic/example unique relative to integration onto main
git rebase --onto main integration topic/example

# Purpose: Adjust commit order, modify commit messages, squash or drop commits
# Conditions: Index and tracked files have no uncommitted modifications; pending commits not yet shared or rewrite coordinated
# Result: Opens a to-do list of commits arranged from oldest to newest, and recreates commits after <upstream> according to the saved content
git rebase -i <upstream>
# Example 1: Interactively tidy up the last 5 commits of the current branch
git rebase -i HEAD~5

# Purpose: Perform rebase when local modifications exist
# Conditions: No unresolved merge conflicts exist; ordinary untracked files must not block Git writes; related commits not shared or rewrite coordinated
# Result: Creates a temporary stash before rebasing, reapplies it after rebasing finishes; reapplying may still produce conflicts
git rebase --autostash <upstream>
# Example 1: Temporarily save local modifications, rebase current branch to main, then reapply these modifications
git rebase --autostash main

# Purpose: Recreate original merge structure during rebase
# Conditions: Index and tracked files have no uncommitted modifications; related commits not shared or rewrite coordinated
# Result: Reapplies ordinary commits on new base and attempts to recreate merge commits; previous manual conflict resolutions may need to be handled again
git rebase --rebase-merges <upstream>
# Example 1: Rebase current branch to main, and recreate its merge structure
git rebase --rebase-merges main

# Purpose: Continue rebase after resolving conflicts or finishing interactive edits
# Conditions: Rebase is paused; conflicts are resolved and staged with git add, or current editing is complete
# Result: Completes the rebuilding of the current commit and continues processing remaining commits
git rebase --continue

# Purpose: Skip currently un-applicable commit and continue rebase
# Conditions: Rebase is paused, and it is confirmed the new history doesn't need the current commit's modifications
# Result: The current commit will not enter the rebuilt history, rebase continues processing subsequent commits
git rebase --skip

# Purpose: Cancel a paused rebase
# Conditions: Rebase has started but not yet finished
# Result: Abandons the unfinished rebase, and restores to the state before the rebase started
git rebase --abort

stash: Temporarily Save Modifications

stash records modifications in the current working directory and index into a local stash, returning the corresponding files to the HEAD state. stash@{0} denotes the newest record, and subsequent records are stash@{1}, stash@{2}, etc. By default, stashes are only saved in the current repository and won’t be sent to remote repositories by a regular push.

Command-line parameters used in this section:

ParameterMeaning
-m <message>, --message <message>Use <message> as the stash description
-u, --include-untrackedSave untracked files as well, but exclude ignored files
-p, --patchWhen used with push, interactively choose modifications to save
-p, --patchWhen used with show, show the full patch
--statShow diff statistics
--End option parsing
# Purpose: View the list of stashes in the current repository
git stash list

# Purpose: Temporarily save modifications to tracked files
# Conditions: Index must not contain unresolved merge conflicts
# Result: Creates a stash, saving staged and unstaged modifications, and returns corresponding files to HEAD state
git stash push -m <message>
# Example 1: Save current modifications to tracked files, and label the record as wip: example
git stash push -m "wip: example"

# Purpose: Save modifications to tracked files and untracked files simultaneously
# Conditions: Index must not contain unresolved merge conflicts
# Result: Creates a stash, saving staged, unstaged modifications, and untracked files; then returns tracked files to HEAD, and removes saved untracked files from working tree; ignored files remain unchanged
git stash push -u -m <message>
# Example 1: Save current modifications and untracked files, but not ignored files
git stash push -u -m "wip: example"

# Purpose: Only save modifications in specified paths
# Conditions: Index must not contain unresolved merge conflicts
# Result: Only writes modifications matching paths to stash and reverts them, other paths remain unchanged
git stash push -m <message> -- <pathspec>...
# Example 1: Only save modifications in docs/ and README.md
git stash push -m "wip: docs" -- docs/ README.md

# Purpose: View diff statistics for a specific stash
# Result: Shows diff stats of the stash relative to its base commit when created, changes no states
git stash show --stat <stash>
# Example 1: View diff stats of the newest stash
git stash show --stat stash@{0}

# Purpose: View full patch of a specific stash
# Result: Shows the full diff of the stash relative to its base commit when created, changes no states
git stash show -p <stash>
# Example 1: View full patch of the newest stash
git stash show -p stash@{0}

# Purpose: Apply a specific stash, while keeping the record
# Conditions: Working directory must match index
# Result: Applies recorded modifications to current state; may produce conflicts, but won't delete the stash
git stash apply <stash>
# Example 1: Apply newest stash, keeping the record
git stash apply stash@{0}

# Purpose: Apply a specific stash, and delete the record upon success
# Conditions: Working directory must match index
# Result: Applies recorded modifications to current state; deletes stash on success, keeps it on conflict
git stash pop <stash>
# Example 1: Apply newest stash, and delete the record upon success
git stash pop stash@{0}

# Purpose: Create a branch from the base commit when the stash was created and apply modifications
# Conditions: <new-branch> must not exist, current local modifications must not prevent switching to the stash's base commit
# Result: Creates and switches to new branch, applies stash; deletes stash record upon success
git stash branch <new-branch> <stash>
# Example 1: Create recovery/example branch and apply newest stash
git stash branch recovery/example stash@{0}

worktree: Manage Multiple Working Trees

worktree allows the same repository to have multiple working directories simultaneously, using different branches or commits in each. The state of each worktree is mutually independent, but they share repository data, so there’s no need to repeatedly clone the repository to work on multiple branches at once. The main worktree is the default repository directory created when cloning or initializing; git worktree add can create more worktrees outside of it.

Main worktree/
├── Working directory files
└── .git/ ($GIT_COMMON_DIR)
    ├── objects/ (shared by all worktrees)
    ├── refs/heads/ (shared by all worktrees)
    ├── refs/tags/ (shared by all worktrees)
    ├── config (shared by default)
    ├── HEAD → main (independent to main worktree)
    ├── index (independent to main worktree)
    └── worktrees/ (subdir name usually takes terminal dir name of worktree, appending numbers on collision; not a commit ID)
        ├── <id-a>/ (corresponds to worktree A)
        │   ├── HEAD → branch-a
        │   └── index
        └── <id-b>/ (corresponds to worktree B)
            ├── HEAD → branch-b
            └── index

worktree A/
├── Working directory files
└── .git (regular file)
    └── Content: gitdir: Main worktree/.git/worktrees/<id-a>

worktree B/
├── Working directory files
└── .git (regular file)
    └── Content: gitdir: Main worktree/.git/worktrees/<id-b>
  • The same local branch can by default only be used in one worktree. Each worktree has its own HEAD, but local branch references are shared by the repository. If you force the HEAD of two worktrees to simultaneously point to the same local branch, creating a commit in one will move the branch forward; the other worktree’s HEAD immediately points to the new commit, but its index and working directory retain the previous contents. At this point Git will show staged differences not resulting from actual edits, and continuing to commit may produce a commit that undoes the previous modification.

Command-line parameters used in this section:

ParameterMeaning
-b <new-branch>Create <new-branch>, and have new worktree use this branch
-d, --detachHave new worktree’s HEAD point directly to a commit, not using a local branch
-n, --dry-runOnly show records that prune would clean up
# Purpose: View all worktrees associated with the repository
git worktree list

# Purpose: Create a new branch from a specified starting point, and create a worktree for it
# Conditions: Working directory where command is executed can be clean or unclean
# Result: Creates a new branch starting from <commit-ish>, and establishes a worktree for it at <path>
git worktree add -b <new-branch> <path> [<commit-ish>]
# Example 1: Create hotfix/example branch starting from origin/main, establish worktree in adjacent project-hotfix/
git worktree add -b hotfix/example ../project-hotfix origin/main
# Example 2: Create feature/example branch starting from current HEAD, establish worktree in adjacent project-feature/
git worktree add -b feature/example ../project-feature
# Example 3: Create hotfix/v1.2.1 branch starting from v1.2.0, establish worktree in adjacent project-hotfix-v1.2.1/
git worktree add -b hotfix/v1.2.1 ../project-hotfix-v1.2.1 v1.2.0

# Purpose: Create a worktree for an existing local branch
# Conditions: Working directory can be clean or unclean; <branch> is not used by other worktrees
# Result: Establishes a worktree using <branch> at <path>; uncommitted modifications and staged content from original working directory are not copied over
git worktree add <path> <branch>
# Example 1: Establish a worktree using branch release/1.x in adjacent project-release/
git worktree add ../project-release release/1.x

# Purpose: Create a temporary worktree not bound to a branch for a specific version, used for inspecting old versions, testing, or executing bisect
# Conditions: Working directory where command is executed can be clean or unclean
# Result: Establishes worktree at <path>, its HEAD points directly to specified commit in detached HEAD state; original working directory state remains unchanged
git worktree add --detach <path> <commit-ish>
# Example 1: Directly use abbreviated commit ID to establish temp worktree pointing to 9f3a2c1 in adjacent project-inspect-commit/
git worktree add --detach ../project-inspect-commit 9f3a2c1
# Example 2: Establish temp worktree pointing to commit referenced by tag v1.2.0 in adjacent project-inspect-tag/
git worktree add --detach ../project-inspect-tag v1.2.0

# Purpose: Remove a linked worktree no longer needed, but keep its branch and commits
# Conditions: Working directory can be clean or unclean; target must be clean, unlocked, and have no submodules
# Result: Deletes <worktree> working directory and its management records, does not delete corresponding branch and commits
git worktree remove <worktree>
# Example 1: Remove adjacent project-hotfix/ worktree
git worktree remove ../project-hotfix

# Purpose: Preview invalid worktree management records that can be cleaned up
# Conditions: Working directory can be clean or unclean; needs to be executed within any worktree of the repository
# Result: Lists management records where working directory no longer exists and meets cleanup conditions, without actually deleting
git worktree prune --dry-run

# Purpose: Clean up invalid worktree management records
# Conditions: Working directory can be clean or unclean; needs to be executed within any worktree of the repository
# Result: Deletes management records where working directory no longer exists and meets cleanup conditions, doesn't delete existing worktrees, branches, or commits
git worktree prune

# Purpose: Repair invalidated associations after manually moving main worktree or linked worktree
# Conditions: Working directories of execution and target can be clean or unclean; new path must be provided if moving linked worktree
# Result: Re-establishes bidirectional association between repository management directory and corresponding worktree, doesn't change files, branches, or commits
git worktree repair [<path>...]
# Example 1: Repair a linked worktree already manually moved to ../project-hotfix/
git worktree repair ../project-hotfix

cherry-pick: Apply Specific Commits

cherry-pick reads the modifications introduced by an existing commit, applies them to the current branch, and by default creates a new commit with the corresponding content but a different commit ID. It is suitable for moving isolated fixes into other branches without merging the entire history leading up to the original commit.

Command-line parameters used in this section:

ParameterMeaning
-n, --no-commitDo not automatically create a commit
# Purpose: Introduce an existing commit into the current branch
# Conditions: Current working tree must be clean
# Result: Applies modifications introduced by target commit, and creates a new commit on current branch
git cherry-pick <commit-ish>
# Example 1: Introduce commit 9f3a2c1 into current branch
git cherry-pick 9f3a2c1

# Purpose: Apply an existing commit's modifications, but don't immediately create a commit
# Conditions: Index must not contain unresolved merge conflicts; existing local modifications must not be overwritten by target modifications
# Result: Writes target modifications to working directory and index, HEAD and current branch remain unchanged
git cherry-pick --no-commit <commit-ish>
# Example 1: Apply modifications of commit 9f3a2c1, check or adjust before manually committing
git cherry-pick --no-commit 9f3a2c1

revert: Revert Specific Commits with a New Commit

revert creates a new commit that undoes the target commit, without deleting or rewriting the original commit, making it suitable for reverting history that has already been shared.

Command-line parameters used in this section:

ParameterMeaning
-m <parent-number>, --mainline <parent-number>Specify the parent commit numbered <parent-number> as the mainline
--no-patchWhen used with git show, do not display file patches
--pretty=rawWhen used with git show, display commit metadata in raw format
# Purpose: Revert modifications introduced by an ordinary commit
# Conditions: Current working tree must be clean
# Result: Creates a new commit that undoes the target commit, original commit and existing history remain unchanged
git revert <commit-ish>
# Example 1: Revert commit 9f3a2c1 with a new commit
git revert 9f3a2c1

# Purpose: Revert a merge commit; due to multiple parents, -m must specify which mainline to keep
# Conditions: Current working tree must be clean; target is a merge commit; <parent-number> corresponds to one of its parents
# Result: Reverses the changes of the merge relative to that parent commit; does not revert that parent, nor delete the original merge commit
git revert -m <parent-number> <merge-commit>
# Example 1: Check parent order first, then keep the mainline represented by the first parent and revert the merge
git show --no-patch --pretty=raw 6d8f2a1
git revert -m 1 6d8f2a1
# Note: Revert only reverses content, original merge relationship still exists; merging again won't automatically restore the reverted content

reset: Reset HEAD and File State

reset moves the current branch or detached HEAD to a specified commit, and decides whether to simultaneously reset the index and working directory based on the mode. It is suitable for tidying up local states that haven’t been shared; if related commits have already been used by others, moving the branch will rewrite the history they rely on.

The meanings of three common command-line parameters are as follows:

ParameterCurrent Branch or HEADIndexWorking Directory
--softMoved to target commitRemains unchangedRemains unchanged
--mixedMoved to target commitReset to target commit contentRemains unchanged
--hardMoved to target commitReset to target commit contentReset to target commit content
# Purpose: Move current branch, while keeping contents in index and working directory
# Conditions: Index must not contain unresolved merge conflicts
# Result: Current branch or detached HEAD moved to target commit; index and working directory remain unchanged
git reset --soft <commit-ish>
# Example 1: Retract current commit, while keeping its modifications in a staged state
git reset --soft HEAD^

# Purpose: Move current branch and cancel staged state after target commit
# Conditions: Current working tree can be clean or unclean
# Result: Current branch or detached HEAD moved to target commit, index reset to target content, working directory remains unchanged
git reset --mixed <commit-ish>
# Example 1: Retract current commit and cancel staged state of its modifications, but keep file modifications
git reset --mixed HEAD^

# Purpose: Make current branch, index, and tracked files all return to specified commit
# Conditions: Current working tree can be clean or unclean
# Result: Current branch or detached HEAD moved to target commit; index and tracked files overwritten by target content, blocking untracked paths might also be deleted
git reset --hard <commit-ish>
# Example 1: Retreat current branch to previous commit, and discard uncommitted modifications in index and working directory
git reset --hard HEAD^

reset --hard will discard uncommitted modifications to tracked files, and should not be used for ordinary cleanup.

reflog: View Reference Log

reflog (reference log) records where branches and other references in the local repository previously pointed. It differs from commit history: commit history saves parent-child relationships between commits, while reflog saves the movement of references due to local operations. git reflog by default shows the reference log of HEAD, which also records branch switches; viewing a specific branch’s reflog only reflects the movements of that branch reference itself.

Reflogs only exist locally and are not synchronized between repositories via fetch, pull, or push. As long as the corresponding record hasn’t expired and the target object still exists, commits no longer pointed to by the current branch or tags can be accessed via reflog.

Reflog positions are expressed using <ref>@{<specifier>}:

NotationMeaning
<ref>@{0}The value of <ref> after the most recent recorded move, usually its current value
<ref>@{<n>}The position <ref> was in <n> moves ago; numbering starts from 0
<ref>@{<date>}The position <ref> was in at a specified time; based on reference update time, not commit creation time
HEAD@{2}The position HEAD was in two moves ago
main@{yesterday}The position local main was in yesterday
main@{one.week.ago}The position local main was in a week ago

Command-line parameters used in this section:

ParameterMeaning
-n <count>Show at most <count> records
--date=<format>Show record times according to <format>
# Purpose: View update records for HEAD or specified reference
# Result: Shows reflog position, commit ID, and operation description in reverse chronological order, changes no references
git reflog show [-n <count>] [--date=<format>] [<ref>]
# Example 1: Show recent 10 update records of HEAD in local time
git reflog show -n 10 --date=local HEAD
# Example 2: Show recent 5 update records of main
git reflog show -n 5 main

# Purpose: List references in current repository that have a reflog
# Result: Outputs reference names, changes no reflog or references
git reflog list

# Purpose: Check if specified reference has a reflog
# Result: Returns exit status 0 if exists, non-zero otherwise; does not output reflog content
git reflog exists <ref>
# Example 1: Check if local main branch has a reflog
git reflog exists refs/heads/main

# Purpose: Inspect an old position recorded in reflog
# Conditions: Specified record has not expired, and the Git object it points to still exists
# Result: Shows the commit corresponding to that position and its patch, does not move any references
git show <ref>@{<specifier>}
# Example 1: Inspect the commit HEAD pointed to two moves ago
git show HEAD@{2}

# Purpose: Retain a commit found in reflog as a new branch
# Conditions: Specified record and its commit still exist; <new-branch> must not exist
# Result: Creates a new branch pointing to that commit, does not switch branches, does not move existing references
git branch <new-branch> <ref>@{<specifier>}
# Example 1: Retain the commit HEAD pointed to two moves ago as rescue/example
git branch rescue/example HEAD@{2}

Reflog is not a permanent backup. By default, records reachable from current references are controlled by gc.reflogExpire, kept for 90 days by default; records unreachable from current references are controlled by gc.reflogExpireUnreachable, kept for 30 days by default. Actual durations can be modified via configuration; after records expire, if the corresponding commits are no longer protected by other references or reflogs, they might later be deleted by garbage collection. Daily viewing and recovery do not require directly executing reflog expire, delete, or drop.

bundle: Bundle Repository History

bundle writes references and their reachable Git objects into a single file, which can transmit repository data offline or via alternative media without network access. Self-contained bundles contain complete history and can be directly cloned; incremental bundles only contain new commits after a designated base, and the receiving repository must already possess the prerequisite commits.

Command-line parameters used in this section:

ParameterMeaning
--allPack all references
# Purpose: Create a self-contained bundle
# Conditions: Execution working directory can be clean or unclean; can pack all references or specified branch
# Result: Writes self-contained repository data into <file>
git bundle create <file> <git-rev-list-args>
# Example 1: Pack all references in current repository into project.bundle
git bundle create project.bundle --all
# Example 2: Only pack history of main branch into main.bundle
git bundle create main.bundle main

# Purpose: Create an incremental bundle
# Conditions: Current repository must possess the specified new commits and base commits
# Result: Writes only the history from <base> to <new-commit> into <file>
git bundle create <file> <base>..<new-commit>
# Example 1: Create an incremental bundle from v1.0.0 up to main
git bundle create update.bundle v1.0.0..main

# Purpose: Verify if a bundle is valid and complete
# Conditions: Execute within a Git repository; when verifying incremental bundles, should execute in receiving repository
# Result: Checks file integrity, and lists prerequisite commits missing in current repository; imports no objects
git bundle verify <file>
# Example 1: Verify project.bundle
git bundle verify project.bundle

# Purpose: Create new repository from a self-contained bundle
# Conditions: <file> must be a bundle with no missing prerequisite commits, <directory> must not be an existing non-empty directory
# Result: Creates new repository in <directory> containing bundle history
git clone <file> <directory>
# Example 1: Create project/ repository from project.bundle
git clone project.bundle project

# Purpose: View references provided by a bundle
# Result: Shows readable references and their commit IDs within, imports no objects
git ls-remote <file>
# Example 1: View references provided by project.bundle
git ls-remote project.bundle

# Purpose: Fetch specified reference from bundle
# Conditions: Execute in receiving repository; repository must possess bundle's prerequisite commits
# Result: Imports required objects, and updates local reference according to refspec
git fetch <file> <source-ref>:<destination-ref>
# Example 1: Fetch main branch of project.bundle as bundle/main remote-tracking reference
git fetch project.bundle refs/heads/main:refs/remotes/bundle/main

archive: Archive Versioned Files

archive generates a source code package from a specified commit, tag, or tree object. By default it only reads tracked files in that version, and does not include .git, uncommitted modifications, or untracked files.

Command-line parameters used in this section:

ParameterMeaning
--format=<format>Set archive format to <format>
--prefix=<prefix>/Prepend <prefix>/ to paths inside the archive
-o <output>, --output=<output>Write archive to <output>
# Purpose: Export specified version as a source package
# Conditions: Execution working directory can be clean or unclean
# Result: Writes files in specified version to <output>, and prepends <prefix>/ to paths inside the package
git archive --format=<format> --prefix=<prefix>/ -o <output> <tree-ish>
# Example 1: Export v1.2.0 tag as project-1.2.0.tar.gz
git archive --format=tar.gz --prefix=project-1.2.0/ -o project-1.2.0.tar.gz v1.2.0

.gitignore indirectly affects archive contents: files ignored and not added to the repository are not in the commit, so naturally git archive won’t export them. But .gitignore cannot exclude files that already exist in the archived version; to exclude such tracked paths from release packages, set export-ignore in the .gitattributes of that version:

.github/** export-ignore
tests/** export-ignore

bisect: Binary Search to Locate Commits Introducing Changes

bisect is used to find the location where state changed between a commit known to be good and a commit known to be bad. Git selects a candidate commit each time; after marking it as good, bad, or skip, Git narrows the range and selects the next candidate commit, until the boundary is located.

The entire process must use the same judgment standard, and the target state should only change from good to bad once within the selected range. Commits that cannot be reliably judged should be marked as skip; if such commits are adjacent to the boundary, Git might only be able to provide multiple candidate commits.

# Purpose: Specify bad and good boundaries and start bisection
# Conditions: Both boundaries have been verified; working directory can safely switch commits
# Result: Records HEAD before starting, and switches to first candidate commit
git bisect start <bad> <good>
# Example 1: Start bisect between current commit and v1.0.0
git bisect start HEAD v1.0.0

# Purpose: Mark current candidate commit as good/normal
# Conditions: bisect in progress, and current commit confirmed good
# Result: Updates good boundary, and switches to next candidate or reports result
git bisect good

# Purpose: Mark current candidate commit as bad/abnormal
# Conditions: bisect in progress, and current commit confirmed bad
# Result: Updates bad boundary, and switches to next candidate or reports result
git bisect bad

# Purpose: Skip candidate commit that cannot currently be reliably judged
# Conditions: bisect in progress, and current commit cannot be classified as good or bad
# Result: Selects other candidate commit; may not determine unique result if skips are near boundary
git bisect skip

# Purpose: End bisection
# Conditions: bisect in progress
# Result: Clears bisection state, and restores branch or commit position before bisection started
git bisect reset

When state can be stably judged using a command or script, git bisect run can automatically test each candidate commit:

Test Command Exit StatusJudgment
0good
1–127, excluding 125bad
125skip
Other valuesAbort bisect
# Purpose: Automatically test candidate commits with specified command
# Conditions: bisect in progress; command can return stable exit status as agreed
# Result: Repeatedly tests and marks candidate commits, until boundary located or bisect aborted
git bisect run <command> [<argument>...]
# Example 1: Automatically test candidates using script outside repository
git bisect start HEAD v1.0.0
git bisect run ../test-regression.sh
git bisect reset

The test command must be executable across candidate commits. It should only return bad when the target state occurs; build failures or environmental issues unrelated to the target should return 125.

# Purpose: Output operation records of current bisection process
# Conditions: bisect in progress
# Result: Outputs records for inspection, saving, or replay usage, changes no bisect state
git bisect log
# Example 1: Save records to a file outside repository
git bisect log > ../bisect.log

# Purpose: Restore bisect progress from existing records
# Conditions: <log-file> is valid record generated by git bisect log; working directory can safely switch commits
# Result: Replays start, good, bad, and skip operations from records
git bisect replay <log-file>
# Example 1: Restore bisect progress from saved records
git bisect reset
git bisect replay ../bisect.log

maintenance: Maintain Repository Data

maintenance is used to tidy up accumulating repository data such as commits, objects, pack files, and references, to shorten the time required for history traversal, object fetching, and reference reading. It is not aimed at modifying project files in the working directory, and the working directory can be clean or unclean when executed.

When maintaining large or frequently updated repositories long-term, you usually don’t need to choose maintenance tasks individually. The most direct entry point is git maintenance start: it adds the current repository to the user-level maintenance list, and establishes a background scheduler shared by all registered repositories. If no maintenance strategy was configured before, Git adopts the incremental strategy, whose actual scheduling is as follows:

FrequencyAutomatically Executed TasksActual Effect
Hourlycommit-graph, prefetchUpdates commit graph; prefetches remote objects to refs/prefetch/, but doesn’t move ordinary remote-tracking branches
Dailyloose-objects, incremental-repackBatches and packs loose objects, gradually merges smaller pack files
Weeklypack-refsTidies loose references, speeds up traversal of large numbers of references

This strategy does not schedule comprehensive gc. prefetch will access the remote repository, but will not update ordinary remote-tracking branches or tags; when executing a routine fetch later, the objects needing transmission will usually be fewer.

Command-line parameters used in this section:

ParameterMeaning
--task=<task>Only run specified task; can be reused, and executes in given order
--autoOnly run when repository state reaches corresponding task’s trigger threshold
--scheduler=<scheduler>Specify auto, crontab, systemd-timer, launchctl, or schtasks for start
--globalWhen used with git config, read current user’s global configuration
--get-all <name>Output all values for configuration item <name>
--unset <name>Delete configuration item <name>
# Purpose: Start scheduled maintenance for current repository
# Conditions: Execution working directory can be clean or unclean; available scheduler exists in OS
# Result: Registers current repository, sets to incremental if no strategy configured, turns off auto-maintenance triggered by regular Git commands, and creates or updates user-level background schedule
git maintenance start [--scheduler=<scheduler>]
# Example 1: Let Git automatically choose scheduler based on OS
git maintenance start --scheduler=auto

# Purpose: Add another repository to maintenance list without altering existing scheduler
# Conditions: Execute in repository to be added; working directory can be clean or unclean
# Result: Registers current repository and completes same repository configuration as start, but doesn't create or start scheduler
git maintenance register
# Example 1: Scheduler already started by other repositories, add adjacent project-b to same maintenance list
cd ../project-b
git maintenance register

# Purpose: View registered repositories
# Result: Outputs all maintenance.repo values in current user's global config
git config --global --get-all maintenance.repo

# Purpose: Stop scheduled maintenance only for current repository
# Conditions: Current repository already registered; working directory can be clean or unclean
# Result: Removes current repository from maintenance list, other repositories and scheduler remain unchanged; maintenance.auto=false is still retained
git maintenance unregister
# Optional: Restore default behavior of regular Git commands triggering auto-maintenance as needed
git config --unset maintenance.auto

# Purpose: Pause scheduled maintenance for all registered repositories
# Conditions: Working directory can be clean or unclean
# Result: Stops and removes shared user-level scheduler, but retains maintenance list; executing start later can continue processing these repositories
git maintenance stop

# Purpose: Manually run specified tasks for explicit issues
# Conditions: Execution working directory can be clean or unclean; each <task> must be a supported task name
# Result: Only executes specified tasks in order of appearance, doesn't run other tasks
git maintenance run --task=<task> [--task=<task>...]
# Example 1: After importing massive commits, immediately update commit-graph
git maintenance run --task=commit-graph
# Example 2: Repository accumulated many loose objects and small pack files, execute incremental repack
git maintenance run --task=loose-objects --task=incremental-repack
# Example 3: When a full tidy is needed, run gc alone; don't put loose-objects in the same maintenance run
git maintenance run --task=gc

# Purpose: Only run task when repository reaches corresponding maintenance threshold
# Conditions: Execution working directory can be clean or unclean
# Result: Executes maintenance when threshold reached, otherwise doesn't alter repository data
git maintenance run --auto

A comprehensive gc might take a long time, and might also clean up data that has passed its retention period and is unreachable. After enabling start, there is usually no need to manually run the above tasks anymore; explicitly specifying --task is mainly used for immediately addressing confirmed repository data issues.

Local and Remote Histories Diverge After Rebasing a Pushed Branch onto the Latest main

This section covers a specific scenario: the commits on feature/example have already been pushed, and the local branch is then rebased onto an updated origin/main. The rebase rewrites only the local history, so the local branch diverges from the old history on the remote. At this point, a regular push is rejected, and pull does not directly solve the problem either.

How This Happens

  1. Create feature/example from main and make two commits, C and D.
  2. Push the branch. At this point, both feature/example and origin/feature/example point to D.
  3. main then advances by 29 commits.
  4. On feature/example, run git fetch origin and git rebase origin/main. Git recreates C′ and D′ after the new base. The local branch now points to D′, while the remote still points to the old D.

Commit Graph After the Rebase

In the graph, M* represents the 29 new commits on main in a collapsed form. The rebase has rewritten only the local history: the remote ref remains on C and D, while the local branch passes through M* and points to the recreated C′ and D′:

A and B are shared history. origin/feature/example still points to C and D, which branch from B. M with an asterisk represents the 29 new commits on origin/main in a collapsed form. The local feature/example points to C prime and D prime, recreated after those commits. B is the common ancestor of the two histories. A B M* C′ D′ C D origin/feature/example origin/main feature/example · HEAD Common ancestor

The commit graph also explains the ahead and behind counts. Check the short-format status:

git status --short --branch

The output will be:

## feature/example...origin/feature/example [ahead 31, behind 2]

ahead and behind count commits by reachability; they do not compare file contents:

  • ahead 31: the 29 commits represented by M*, plus the local C′ and D′.
  • behind 2: the old remote-only commits C and D.

More generally, suppose main gains N commits after the branches diverge and all K commits already on the feature branch are rebased. If no other divergence exists, the status will show ahead N + K and behind K. Here, behind 2 confirms that the commit graph has diverged, but the number alone does not mean that someone added new work to the remote.

What Happens If You Run git push Now

git push origin feature/example

A regular push is rejected. The output usually includes:

! [rejected] feature/example -> feature/example (non-fast-forward)
error: failed to push some refs to '<remote-url>'

A regular push permits only a fast-forward update: the remote branch tip before the push must be an ancestor of the commit being pushed. Here, the remote tip is D, while the local tip D′ is on a different line of history. D is not an ancestor of D′. This check still fails even if D and D′ ultimately introduce the same file changes.

What Happens If You Run git pull Now

git pull

pull first fetches the remote state, then tries to integrate origin/feature/example into the current branch. Because the two sides have diverged, what happens next depends on the pull strategy:

pull strategyWhat happens
Neither merge nor rebase is configuredGit stops and reports Need to specify how to reconcile divergent branches
pull.ff=only or git pull --ff-onlyGit stops and reports Not possible to fast-forward
mergeGit tries to merge D and D′; this may cause conflicts, and a successful merge leaves a merge commit connecting both the old and new histories
rebaseGit uses the old origin/feature/example as the base and replays the local-only commits again; this may cause conflicts or produce an unexpected history

When pull.rebase=true is configured, git pull takes the last path. If conflicts occur, the output usually includes CONFLICT and could not apply, and the repository is left in an unfinished rebase.

All of these behaviors try to integrate the old remote history. What this scenario actually requires is replacing C and D with C′ and D′. Therefore, even if pull can complete, it is not the correct way to handle this situation.

If an accidental pull is currently stopped at a conflict, abort the operation that is actually in progress:

# If pull is using rebase
git rebase --abort

# If pull is using merge
git merge --abort

If pull failed before integration began, there is nothing to abort. If it has already completed, first use reflog to recover the branch position from before the pull, then continue with the steps below.

Resolution

The correct goal is to move the remote feature/example from the old commit D to the rebased D′. This requires that the branch may be rewritten and that the remote contains no new work from anyone else.

First update the remote-tracking ref and inspect the remote-only commits:

git fetch origin

# View the complete graph after the histories diverged
git log --graph --oneline --decorate --boundary \
  feature/example...origin/feature/example

# View only commits that exist on the remote but not locally
git log --oneline \
  feature/example..origin/feature/example

# Check whether those remote commits have patch-equivalent local versions
git cherry -v \
  feature/example origin/feature/example

In this example, the only remote-only commits should be the pre-rebase C and D. git cherry will usually mark them with -, indicating that patch-equivalent C′ and D′ exist locally. A - is only supporting evidence; use the commit IDs, messages, and collaboration context to verify that these really are the old commits you intend to replace.

Once verified, update the remote with a protected force push:

# Purpose: Replace the old remote history with the rebased local history
# Conditions: You have fetched and confirmed that the remote contains only the pre-rebase commits; the branch may be rewritten
# Result: If the protective checks pass, updates origin/feature/example to the local feature/example
git push \
  --force-with-lease \
  --force-if-includes \
  origin feature/example

This push command contains two layers of protection: --force-with-lease checks whether the remote branch is still at the expected position, while --force-if-includes checks whether that remote position was genuinely part of the local branch’s history. Before examining these checks, distinguish the three refs in this example:

LocationPoints toMeaning
Server-side feature/exampleDThe remote branch that the push will actually change
Local origin/feature/exampleDThe remote position recorded by the most recent fetch
Local feature/exampleD′The new rebased history ready to be pushed

Think of --force-with-lease as saying, “I agree to overwrite only the remote version I expect.”

No expected value is specified here, so Git uses the value of the local origin/feature/example, D, as the expectation. The server compares the values atomically when updating the ref:

  • The server still points to D: the remote branch has not moved away from the state recorded locally, so the check passes and Git may proceed with trying to update it to D′.
  • The server already points to X pushed by someone else: X differs from the locally recorded D, so the check fails and the remote remains unchanged.

This check does not compare file contents or determine whether C and D really are obsolete copies. You must establish those facts during the preceding history inspection.

--force-with-lease still has a gap: after someone pushes X, an editor might automatically run fetch and update the local origin/feature/example to X as well. The server and the local record now match again, so the lease alone would pass even though you may never have seen or handled X.

--force-if-includes addresses this gap. It requires the reflog of the local feature/example to show that the current remote-tracking tip was previously contained in that branch’s history, rather than merely recorded by fetch:

  • In this example, D was the tip of feature/example before the rebase, and the branch reflog retains that position, so the check passes.
  • If X was only fetched in the background into origin/feature/example, the local feature branch never contained X, so the check fails.
  • If you deliberately integrated X into the local branch, the ancestry relationship and reflog can establish that fact, allowing the check to pass.

Together, the checks produce the following results:

Situation--force-with-lease--force-if-includesResult
The remote is still at the old commit Dpassespassesupdate allowed
Someone pushed X, and you have not fetched itfailsno need to continueupdate rejected
A background fetch retrieved X, but you did not integrate itpassesfailsupdate rejected
You deliberately integrated X locallypassespassesupdate allowed

“Update allowed” means only that these two protective checks have passed; it does not mean that Git has determined your intended outcome for you. If either check fails, fetch and inspect again. Do not switch to an unconditional --force.

After a successful push, verify the result:

git fetch origin
git status --short --branch

The expected output no longer contains ahead or behind:

## feature/example...origin/feature/example

Pinning an Explicit Expected Value for the Lease

In the previous command, the lease expectation comes from origin/feature/example, and a later fetch may update that ref. To pin the push condition to “the server-side branch must still point to the D that I just inspected,” explicitly provide the commit ID of D:

# Print and manually verify the remote tip; in this example, it should be the old pre-rebase commit D
git rev-parse origin/feature/example

# Substitute the full commit ID from the previous command for <expected-remote-tip>
git push \
  --force-with-lease=refs/heads/feature/example:<expected-remote-tip> \
  origin feature/example:feature/example

This form directly compares the server-side refs/heads/feature/example with the specified commit ID and no longer depends on a remote-tracking ref that might change. The update is accepted only if the two values match; otherwise, the remote remains unchanged.

When --force-with-lease=<ref>:<expect> is used, --force-if-includes has no effect and should not be combined with it. If the server-side check fails, fetch and inspect the remote history again instead of switching to --force.

When the Remote Contains New Commits from Other Sources

If git log feature/example..origin/feature/example shows unfamiliar commits other than C and D, or if git cherry produces an unexplained +, stop the force push. You can no longer assume that the remote contains only the old history you intend to replace.

What to do next depends on which history you need to preserve:

GoalApproach
Preserve the complete existing remote historyMerge origin/feature/example, resolve any conflicts, then push normally
Keep the rebased history linearCoordinate with the commit authors, cherry-pick only the confirmed new commits onto D′, then use a protected force push
The branch is protected or may not be rewrittenPush the rebased history as a new branch and handle it through a pull request

To preserve the complete remote history:

# Conditions: The working tree has no uncommitted changes that would interfere with the merge
git merge origin/feature/example
git push origin feature/example

The merge commit contains both the local and remote tips, so a regular push can fast-forward the remote. The tradeoff is that both the pre-rebase and post-rebase feature branch histories are retained.

If you need only particular new commits from the remote, select them explicitly:

git cherry-pick <remote-new-commit>...

After resolving conflicts and running tests, repeat the remote-history inspection from this section, then use the protected force push. Do not mechanically run git rebase origin/feature/example: that command chooses the old remote feature history as the new base and may also replay the batch of main commits as local-only commits, defeating the original goal of basing the feature branch on the latest main.

If the branch may not be rewritten, keep the current rebased result and push it as a new branch:

git switch -c feature/example-rebased
git push --set-upstream origin feature/example-rebased

Reducing This Kind of Divergence

  • Before creating a feature branch, update the remote-tracking refs and start from the latest origin/main:

    git fetch origin
    git switch -c feature/example origin/main
  • Keep feature branches short-lived to reduce the need to rewrite already-pushed history just to catch up with main.

  • On a personal branch that may be rewritten, treat “rebase onto the latest main” and “force-push with a lease” as a single operation. Do not run Pull or Sync between them.

  • For a shared branch whose history should not be rewritten, merge the latest main to preserve the existing commit IDs:

    git fetch origin
    git merge origin/main
    git push origin feature/example
  • If the hosting platform prohibits force pushes, follow its branch-protection rules. When you need a new linear history, push it to a new branch and handle it through a pull request.