Git 操作备忘录
面向实际工作场景的 Git 操作速查,帮助快速找到实用但不易记忆的命令,并明确其使用条件与执行影响。
- 首次发布
- 最后更新
本文目录
基础记号与术语
命令格式中的记号
| 记号 | 含义 |
|---|---|
<name> | 需要替换为实际值的占位符;尖括号本身不输入 |
[<name>] | 可以省略的占位符;方括号本身不输入 |
<name>... | 可以提供一个或多个同类值;省略号本身不输入 |
A | B | 从 A 和 B 中选择一个;竖线本身不输入 |
-ish
英语后缀 -ish 表示“类似……的”或“大致属于……的”。Git 文档用它命名一类能够被解析为特定对象的表达式,它不是需要输入的命令参数。
commit-ish 表示能够最终解析为某个提交的名称或表达式,例如:
main # 分支v1.0.0 # 指向提交的标签a1b2c3d # commit IDHEAD # 当前提交HEAD~2 # 当前提交的前两代祖先origin/main # 远程跟踪分支因此,[<commit-ish>] 表示这里可以填写分支、标签、commit ID 或 HEAD 表达式,也可以省略。
tree-ish 表示能够最终解析为树对象的名称或表达式。分支、提交和指向提交的标签都可以继续解析到该提交记录的项目文件树,因此 main、v1.0.0、HEAD、树对象 ID 和 HEAD^{tree} 都可以作为 tree-ish。
HEAD
HEAD 是一个特殊引用:正常状态下指向当前分支;detached HEAD 状态下直接指向某个提交。
当前分支为 main 时,从 clean 状态开始修改文件、暂存修改和创建提交,都不会改变 HEAD → main 这一引用关系:
如果直接切换到某个 commit ID,HEAD 将不再指向分支,而是直接指向该提交。这称为 detached HEAD,即 HEAD 与分支脱离:
HEAD → <commit-id>detached HEAD 状态下仍可修改、暂存和提交,但创建提交时不会使任何本地分支前移。需要保留这些提交时,应在离开前执行 git switch -c <new-branch> 创建分支。
执行普通的 git commit 时,新提交会记录当前分支原来所在的提交;这个紧接在新提交之前的提交称为它的父提交。历史起点的根提交没有父提交,普通提交通常有一个父提交,合并多段历史产生的提交则可以有多个父提交。^ 和 ~ 都利用这种关系向前查找提交。
两者都从一个给定提交出发,但查找规则不同:
<commit-ish>^<n>:选择<commit-ish>的第n个父提交,n从1开始;省略<n>时选择第一个父提交。<commit-ish>~<n>:从<commit-ish>开始,连续选择第一个父提交n次;省略<n>时只选择一次。
因此,^2 表示“第二个父提交”,~2 才表示“沿第一父提交链向前追溯两代”;<commit-ish>^、<commit-ish>^1、<commit-ish>~ 和 <commit-ish>~1 表示同一个提交。
例如,feature 从 main 的 C1 处分出。在 main 上执行 git merge feature 后产生合并提交 M:
图中每条有向连线都从父提交指向由它产生的后续提交。
第一、第二不是根据图中的位置或分支名称推断出来的,而是合并提交对象中多条 parent 记录的先后顺序。在 main 上执行 git merge feature 时:
- 合并前,
HEAD指向main,main指向C2;Git 将C2写入第一条parent记录。 feature指向F2;合并期间,Git 使用MERGE_HEAD记录这个被合入的提交,并将F2写入第二条parent记录。
因此,合并提交 M 的相关内容等价于:
parent <C2 的 commit ID>parent <F2 的 commit ID>图中由 C2 和 F2 指向 M 的两条有向连线对应这两条记录。图中的 HEAD^、HEAD^2 和 HEAD~2 均以合并完成后仍停留在 main 为前提,此时 HEAD 指向合并提交 M。将前述规则应用于此时的 HEAD:
HEAD^:取M排在第一的父提交,得到C2;HEAD^2:数字2表示取M排在第二的父提交,得到F2,不是向前追溯两代;HEAD~2:连续两次取排在第一的父提交,经过M → C2 → C1,得到C1。
这些记号会从使用它们时 HEAD 所指向的提交开始查找,并不固定表示图中的某个提交。若随后切换到 feature,HEAD 将指向 F2,此时 HEAD^ 表示 F2 的第一父提交 F1;合并提交 M 的父提交关系并未改变,只是不再以 HEAD 为起点访问它。
如果改为在 feature 上执行 git merge main,合并前的 HEAD 是 F2,所以第一条记录会是 F2,第二条记录才是被合入的 C2。
合并提交并不只限于两个父提交。在 main 上同时合入多个分支时,例如:
git merge feature-a feature-b如果该命令成功生成一个多分支合并提交,其提交对象可以包含三条 parent 记录:
parent <合并前 main 所在提交的 commit ID>parent <feature-a 所在提交的 commit ID>parent <feature-b 所在提交的 commit ID>这种合并称为 Octopus merge(多分支合并)。此时,HEAD^3 读取第三条 parent 记录;它与 HEAD~3 不同,后者会沿第一条 parent 记录连续向前查找三代。提交可以记录任意数量的父提交,具体顺序可直接查看提交对象:
git cat-file -p HEAD工作区状态
工作区(working tree,官方中文资料也称“工作目录”)是仓库对应目录中供编辑的实际项目文件。修改文件会直接改变工作区;新建但尚未交给 Git 跟踪的文件会在这里显示为未跟踪文件。
暂存区(index,也称 staging area)保存下一次提交所使用的文件快照信息,并不是一个供人直接编辑的目录。git add 将指定文件当时的内容写入暂存区,git commit 再根据暂存区创建新的提交。
前文介绍的 HEAD 提供当前提交的文件快照。依据 Git 官方 git-status 文档,常见状态可以归纳为以下几组:
| 常见状态 | 工作区 | 暂存区 | git status 官方分类 |
|---|---|---|---|
clean | 已跟踪文件与暂存区一致;除被忽略文件外,没有未跟踪文件 | 与 HEAD 一致 | working tree clean |
| 已暂存修改 | 可能与暂存区一致,也可能在暂存后又被修改 | 保存下一次提交将包含的修改,与 HEAD 不同 | Changes to be committed |
| 未暂存修改 | 已跟踪文件包含尚未写入暂存区的修改 | 尚未保存工作区中的最新内容,也可能保存着更早暂存的内容 | Changes not staged for commit |
| 未跟踪文件 | 存在 Git 尚未跟踪的新路径 | 没有该路径的记录 | Untracked files |
| 未合并路径 | 冲突处理尚未完成 | 可以保存同一路径的多个待合并版本 | Unmerged paths |
除 clean 外,其他状态可以同时出现。例如,文件暂存后又被修改,会同时具有已暂存修改和未暂存修改。
switch:切换分支
git switch <target-branch> 尝试让当前 worktree 改用目标分支。切换时已有本地状态的处理过程如下:
restore:取消暂存
本节使用的命令行参数:
| 参数 | 含义 |
|---|---|
-S、--staged | 将恢复目标设为暂存区 |
# 用途:取消指定路径的暂存状态# 结果:将暂存区中的对应内容恢复为 HEAD 的版本,工作目录文件保持不变git restore --staged <pathspec>...# 示例 1:取消暂存 README.md,但保留文件修改git restore --staged README.mdrebase:在新基线上重放提交
rebase 将一个提交序列改接到新的基线。执行 git rebase <upstream> 时,Git 找出当前分支中 <upstream> 没有的提交,在 <upstream> 之后依次重新创建这些提交,最后让当前分支指向重建后的序列;<upstream> 本身不会移动。
C' 和 D' 是根据 C、D 的修改重新创建的提交。由于父提交发生变化,它们具有新的 commit ID。变基适合整理尚未共享的本地历史;如果其他分支或使用者已经基于这些提交继续工作,应先协调再改写。
本节使用的命令行参数:
| 参数 | 含义 |
|---|---|
--onto <new-base> | 将重建提交的起点改为 <new-base> |
-i、--interactive | 在重建前编辑提交顺序和处理方式 |
--autostash | 变基前临时保存本地修改,结束后重新应用 |
-r、--rebase-merges | 尝试在新基线上重新创建原有合并结构 |
--continue | 解决冲突或完成编辑后继续变基 |
--skip | 跳过当前正在应用的提交 |
--abort | 中止变基并恢复开始前的分支、暂存区和已跟踪工作目录内容 |
# 用途:将当前分支独有的提交重新应用到新的基线# 条件:当前分支是需要改写的分支;暂存区和已跟踪文件没有未提交修改;相关提交尚未共享或已协调改写# 结果:在 <upstream> 之后重建当前分支的提交,并让当前分支指向新的提交序列;<upstream> 不变git rebase <upstream># 示例 1:当前分支为 feature/example,将其独有提交重新应用到 main 之后git rebase main
# 用途:分别指定新基线、提交选择边界和待改写分支# 条件:暂存区和已跟踪文件没有未提交修改;<branch> 未被其他 worktree 使用;相关提交尚未共享或已协调改写# 结果:选择 <branch> 相对于 <upstream> 独有的提交,将其重新应用到 <new-base>,再让 <branch> 指向新序列git rebase --onto <new-base> <upstream> <branch># 示例 1:将 topic/example 相对于 integration 独有的提交重新应用到 maingit rebase --onto main integration topic/example
# 用途:调整提交顺序、修改提交信息、合并或删除提交# 条件:暂存区和已跟踪文件没有未提交修改;待处理提交尚未共享或已协调改写# 结果:打开按提交历史从旧到新排列的待办列表,并按照保存后的内容重新创建 <upstream> 之后的提交git rebase -i <upstream># 示例 1:交互式整理当前分支最近五个提交git rebase -i HEAD~5
# 用途:在存在本地修改时执行变基# 条件:不存在尚未解决的合并冲突;普通未跟踪文件不得阻碍 Git 写入;相关提交尚未共享或已协调改写# 结果:变基前创建临时 stash,变基结束后重新应用;重新应用时仍可能产生冲突git rebase --autostash <upstream># 示例 1:临时保存本地修改,将当前分支变基到 main,再重新应用这些修改git rebase --autostash main
# 用途:变基时重新创建原有合并结构# 条件:暂存区和已跟踪文件没有未提交修改;相关提交尚未共享或已协调改写# 结果:在新基线上重新应用普通提交并尝试重新创建合并提交;原有人工冲突解决可能需要再次处理git rebase --rebase-merges <upstream># 示例 1:将当前分支变基到 main,并重新创建其合并结构git rebase --rebase-merges main
# 用途:解决冲突或完成交互式编辑后继续变基# 条件:rebase 已暂停;冲突已经解决并用 git add 写入暂存区,或当前编辑已经完成# 结果:完成当前提交的重建并继续处理剩余提交git rebase --continue
# 用途:跳过当前无法应用的提交并继续变基# 条件:rebase 已暂停,并已确认新历史不需要当前提交的修改# 结果:当前提交不会进入重建后的历史,rebase 继续处理后续提交git rebase --skip
# 用途:取消处于暂停状态的 rebase# 条件:rebase 已开始但尚未完成# 结果:放弃尚未完成的变基,并恢复到变基开始前的状态git rebase --abortstash:临时保存修改
stash 将当前工作目录和暂存区中的修改记录到本地 stash 中,使相应文件恢复到 HEAD 的状态。stash@{0} 表示最新记录,后续记录依次为 stash@{1}、stash@{2}。stash 默认只保存在当前仓库,不会随普通 push 发送到远程仓库。
本节使用的命令行参数:
| 参数 | 含义 |
|---|---|
-m <message>、--message <message> | 将 <message> 用作 stash 说明 |
-u、--include-untracked | 将未跟踪文件一并保存,但不包含被忽略文件 |
-p、--patch | 用于 push 时,交互选择要保存的修改 |
-p、--patch | 用于 show 时,显示完整补丁 |
--stat | 显示差异统计 |
-- | 结束选项解析 |
# 用途:查看当前仓库中的 stash 列表git stash list
# 用途:临时保存已跟踪文件的修改# 条件:暂存区中不能存在尚未解决的合并冲突# 结果:创建 stash,保存已暂存和未暂存修改,并使相应文件恢复到 HEAD 的状态git stash push -m <message># 示例 1:保存当前已跟踪文件的修改,并将该记录标记为 wip: examplegit stash push -m "wip: example"
# 用途:同时保存已跟踪文件的修改和未跟踪文件# 条件:暂存区中不能存在尚未解决的合并冲突# 结果:创建 stash,保存已暂存修改、未暂存修改和未跟踪文件;随后使相应已跟踪文件恢复到 HEAD,并从工作目录移除已保存的未跟踪文件;被忽略文件保持不变git stash push -u -m <message># 示例 1:保存当前修改和未跟踪文件,但不保存被忽略文件git stash push -u -m "wip: example"
# 用途:只保存指定路径中的修改# 条件:暂存区中不能存在尚未解决的合并冲突# 结果:只将匹配路径的修改写入 stash 并撤回,其他路径保持不变git stash push -m <message> -- <pathspec>...# 示例 1:只保存 docs/ 和 README.md 中的修改git stash push -m "wip: docs" -- docs/ README.md
# 用途:查看指定 stash 的差异统计# 结果:显示该 stash 相对于创建时基线的差异统计,不改变任何状态git stash show --stat <stash># 示例 1:查看最新 stash 的差异统计git stash show --stat stash@{0}
# 用途:查看指定 stash 的完整补丁# 结果:显示该 stash 相对于创建时基线的完整差异,不改变任何状态git stash show -p <stash># 示例 1:查看最新 stash 的完整补丁git stash show -p stash@{0}
# 用途:恢复指定 stash,同时保留该记录# 条件:工作目录必须与暂存区一致# 结果:将记录的修改应用到当前状态;可能产生冲突,但不会删除 stashgit stash apply <stash># 示例 1:恢复最新 stash,同时保留该记录git stash apply stash@{0}
# 用途:恢复指定 stash,并在成功后删除该记录# 条件:工作目录必须与暂存区一致# 结果:将记录的修改应用到当前状态;成功时删除 stash,发生冲突时保留git stash pop <stash># 示例 1:恢复最新 stash,并在成功后删除该记录git stash pop stash@{0}
# 用途:从创建 stash 时的基线建立分支并恢复修改# 条件:<new-branch> 尚不存在,当前本地修改不得妨碍切换到 stash 的基线提交# 结果:创建并切换到新分支,应用 stash;应用成功时删除该 stash 记录git stash branch <new-branch> <stash># 示例 1:建立 recovery/example 分支并恢复最新 stashgit stash branch recovery/example stash@{0}worktree:管理多个工作目录
worktree 可以让同一个仓库同时拥有多个工作目录,并在其中分别使用不同的分支或提交。各 worktree 的状态相互独立,但共用仓库数据,因此不必为了同时处理多个分支而重复克隆仓库。主 worktree 是克隆或初始化仓库时默认建立的仓库目录;git worktree add 可以在此之外创建更多 worktree。
主 worktree/├── 工作目录文件└── .git/($GIT_COMMON_DIR) ├── objects/(所有 worktree 共享) ├── refs/heads/(所有 worktree 共享) ├── refs/tags/(所有 worktree 共享) ├── config(默认共享) ├── HEAD → main(主 worktree 独立) ├── index(主 worktree 独立) └── worktrees/(子目录名通常取 worktree 的末级目录名,重名时追加数字;不是提交 ID) ├── <id-a>/(对应 worktree A) │ ├── HEAD → branch-a │ └── index └── <id-b>/(对应 worktree B) ├── HEAD → branch-b └── index
worktree A/├── 工作目录文件└── .git(普通文件) └── 内容:gitdir: 主 worktree/.git/worktrees/<id-a>
worktree B/├── 工作目录文件└── .git(普通文件) └── 内容:gitdir: 主 worktree/.git/worktrees/<id-b>- 同一本地分支默认只能在一个 worktree 中使用。各 worktree 分别拥有自己的
HEAD,但本地分支引用由仓库共享。若强制让两个 worktree 的HEAD同时指向同一本地分支,其中一个创建提交会使该分支向前移动;另一个 worktree 的HEAD随即指向新提交,但其暂存区和工作目录仍保留原来的内容。此时 Git 会显示并非由实际编辑产生的已暂存差异,继续提交还可能产生撤销前一处修改的提交。
本节使用的命令行参数:
| 参数 | 含义 |
|---|---|
-b <new-branch> | 创建 <new-branch>,并让新 worktree 使用该分支 |
-d、--detach | 让新 worktree 的 HEAD 直接指向提交,不使用本地分支 |
-n、--dry-run | 仅显示 prune 将清理的记录 |
# 用途:查看仓库关联的所有 worktreegit worktree list
# 用途:从指定起点新建分支,并为该分支创建 worktree# 条件:执行命令所在的工作区可以是 clean 或 unclean# 结果:以 <commit-ish> 为起点创建新分支,并在 <path> 中建立该分支的 worktreegit worktree add -b <new-branch> <path> [<commit-ish>]# 示例 1:以 origin/main 为起点创建 hotfix/example 分支,并在相邻的 project-hotfix/ 目录中建立该分支的 worktreegit worktree add -b hotfix/example ../project-hotfix origin/main# 示例 2:以当前 HEAD 为起点创建 feature/example 分支,并在相邻的 project-feature/ 目录中建立该分支的 worktreegit worktree add -b feature/example ../project-feature# 示例 3:以 v1.2.0 为起点创建 hotfix/v1.2.1 分支,并在相邻的 project-hotfix-v1.2.1/ 目录中建立该分支的 worktreegit worktree add -b hotfix/v1.2.1 ../project-hotfix-v1.2.1 v1.2.0
# 用途:为已有的本地分支创建 worktree# 条件:执行命令所在的工作区可以是 clean 或 unclean;<branch> 未被其他 worktree 使用# 结果:在 <path> 中建立使用 <branch> 的 worktree;原工作区中的未提交修改和暂存内容不会复制过去git worktree add <path> <branch># 示例 1:在相邻的 project-release/ 目录中建立使用 release/1.x 分支的 worktreegit worktree add ../project-release release/1.x
# 用途:为指定版本创建不绑定分支的临时 worktree,用于检查旧版本、测试或执行 bisect# 条件:执行命令所在的工作区可以是 clean 或 unclean# 结果:在 <path> 中建立 worktree,其 HEAD 直接指向指定提交并处于 detached HEAD 状态;原工作区状态不变git worktree add --detach <path> <commit-ish># 示例 1:直接使用缩写提交 ID,在相邻的 project-inspect-commit/ 目录中建立指向提交 9f3a2c1 的临时 worktreegit worktree add --detach ../project-inspect-commit 9f3a2c1# 示例 2:在相邻的 project-inspect-tag/ 目录中建立指向 v1.2.0 标签所指提交的临时 worktreegit worktree add --detach ../project-inspect-tag v1.2.0
# 用途:删除不再需要的 linked worktree,但保留其分支和提交# 条件:执行命令所在的工作区可以是 clean 或 unclean;目标必须是 clean、未锁定且不含子模块的 linked worktree# 结果:删除 <worktree> 工作目录及其管理记录,不删除对应分支和提交git worktree remove <worktree># 示例 1:删除相邻的 project-hotfix/ worktreegit worktree remove ../project-hotfix
# 用途:预览可以清理的失效 worktree 管理记录# 条件:执行命令所在的工作区可以是 clean 或 unclean;需在该仓库的任一 worktree 中执行# 结果:列出工作目录已经不存在且符合清理条件的管理记录,不实际删除git worktree prune --dry-run
# 用途:清理失效的 worktree 管理记录# 条件:执行命令所在的工作区可以是 clean 或 unclean;需在该仓库的任一 worktree 中执行# 结果:删除工作目录已经不存在且符合清理条件的管理记录,不删除现存 worktree、分支或提交git worktree prune
# 用途:修复手动移动主 worktree 或 linked worktree 后失效的关联# 条件:执行命令所在的工作区和待修复的工作区都可以是 clean 或 unclean;移动 linked worktree 后需提供其新路径# 结果:重新建立仓库管理目录与相应 worktree 的双向关联,不改变工作目录文件、分支或提交git worktree repair [<path>...]# 示例 1:修复已经手动移动到 ../project-hotfix/ 的 linked worktreegit worktree repair ../project-hotfixcherry-pick:应用指定提交
cherry-pick 读取已有提交引入的修改,将其应用到当前分支,并默认创建内容相应但 commit ID 不同的新提交。它适合把独立修复移入其他分支,不会把原提交之前的整段历史一并合入。
本节使用的命令行参数:
| 参数 | 含义 |
|---|---|
-n、--no-commit | 不自动创建提交 |
# 用途:将一个已有提交引入当前分支# 条件:当前工作区必须是 clean# 结果:应用目标提交引入的修改,并在当前分支创建新提交git cherry-pick <commit-ish># 示例 1:将提交 9f3a2c1 引入当前分支git cherry-pick 9f3a2c1
# 用途:应用已有提交的修改,但不立即创建提交# 条件:暂存区中不能存在尚未解决的合并冲突;现有本地修改不得被目标修改覆盖# 结果:将目标修改写入工作目录和暂存区,HEAD 与当前分支保持不变git cherry-pick --no-commit <commit-ish># 示例 1:应用提交 9f3a2c1 的修改,检查或调整后再自行提交git cherry-pick --no-commit 9f3a2c1revert:以新提交撤销指定提交
revert 创建一个抵消目标提交的新提交,不删除或改写原提交,适合撤销已经共享的历史。
本节使用的命令行参数:
| 参数 | 含义 |
|---|---|
-m <parent-number>、--mainline <parent-number> | 将编号为 <parent-number> 的父提交指定为主线 |
--no-patch | 用于 git show 时,不显示文件补丁 |
--pretty=raw | 用于 git show 时,以 raw 格式显示提交元数据 |
# 用途:撤销普通提交引入的修改# 条件:当前工作区必须是 clean# 结果:创建一个抵消目标提交的新提交,原提交和既有历史保持不变git revert <commit-ish># 示例 1:以新提交撤销提交 9f3a2c1git revert 9f3a2c1
# 用途:撤销合并提交;因其有多个父提交,必须用 -m 指定保留哪条主线# 条件:当前工作区必须是 clean;目标是合并提交;<parent-number> 对应其一个父提交# 结果:反向应用合并相对于该父提交的变化;不是撤销该父提交,也不删除原合并提交git revert -m <parent-number> <merge-commit># 示例 1:先核对 parent 顺序,再保留第一父提交所代表的主线并撤销该次合并git show --no-patch --pretty=raw 6d8f2a1git revert -m 1 6d8f2a1# 注意:撤销只反转内容,原合并关系仍存在;再次合并不会自动恢复被撤销的内容reset:重置 HEAD 与文件状态
reset 将当前分支或 detached HEAD 移到指定提交,并根据模式决定是否同时重置暂存区和工作目录。它适合整理尚未共享的本地状态;若相关提交已经被他人使用,移动分支会改写对方所依赖的历史。
三个常用命令行参数的含义如下:
| 参数 | 当前分支或 HEAD | 暂存区 | 工作目录 |
|---|---|---|---|
--soft | 移到目标提交 | 保持不变 | 保持不变 |
--mixed | 移到目标提交 | 重置为目标提交内容 | 保持不变 |
--hard | 移到目标提交 | 重置为目标提交内容 | 重置为目标提交内容 |
# 用途:移动当前分支,同时保留暂存区和工作目录中的内容# 条件:暂存区中不能存在尚未解决的合并冲突# 结果:当前分支或 detached HEAD 移到目标提交;暂存区和工作目录保持不变git reset --soft <commit-ish># 示例 1:撤回当前提交,同时让其中的修改继续保持已暂存状态git reset --soft HEAD^
# 用途:移动当前分支并取消目标提交之后的暂存状态# 条件:当前工作区可以是 clean 或 unclean# 结果:当前分支或 detached HEAD 移到目标提交,暂存区重置为目标内容,工作目录保持不变git reset --mixed <commit-ish># 示例 1:撤回当前提交并取消其修改的暂存状态,但保留文件修改git reset --mixed HEAD^
# 用途:使当前分支、暂存区和已跟踪文件全部回到指定提交# 条件:当前工作区可以是 clean 或 unclean# 结果:当前分支或 detached HEAD 移到目标提交;暂存区和已跟踪文件被目标内容覆盖,阻碍写入的未跟踪路径也可能被删除git reset --hard <commit-ish># 示例 1:将当前分支退回前一个提交,并丢弃暂存区和工作目录中的未提交修改git reset --hard HEAD^reset --hard 会丢弃未提交的已跟踪文件修改,不应用于普通清理。
reflog:查看引用日志
reflog(reference log,引用日志)记录本地仓库中分支及其他引用曾经指向的位置。它与提交历史不同:提交历史保存提交之间的父子关系,reflog 保存引用随本地操作发生的移动。git reflog 默认显示 HEAD 的引用日志,其中还会记录分支切换;查看某个分支的 reflog,则只反映该分支引用自身的移动。
reflog 只存在于本地,不会通过 fetch、pull 或 push 在仓库之间同步。只要相应记录尚未过期且目标对象仍然存在,就可以通过 reflog 访问已经不再被当前分支或标签指向的提交。
reflog 位置使用 <ref>@{<specifier>} 表示:
| 记号 | 含义 |
|---|---|
<ref>@{0} | <ref> 最近一次记录后的值,通常就是当前值 |
<ref>@{<n>} | <ref> 向前数第 <n> 次移动前所在的位置;编号从 0 开始 |
<ref>@{<date>} | <ref> 在指定时间所处的位置;依据引用更新时间,而不是提交创建时间 |
HEAD@{2} | HEAD 在两次移动之前所处的位置 |
main@{yesterday} | 本地 main 在昨天所处的位置 |
main@{one.week.ago} | 本地 main 在一周前所处的位置 |
本节使用的命令行参数:
| 参数 | 含义 |
|---|---|
-n <count> | 最多显示 <count> 条记录 |
--date=<format> | 按 <format> 显示记录的时间 |
# 用途:查看 HEAD 或指定引用的更新记录# 结果:按时间倒序显示 reflog 位置、commit ID 和操作说明,不改变任何引用git reflog show [-n <count>] [--date=<format>] [<ref>]# 示例 1:以本地时间显示 HEAD 最近 10 条更新记录git reflog show -n 10 --date=local HEAD# 示例 2:显示 main 最近 5 条更新记录git reflog show -n 5 main
# 用途:列出当前仓库中拥有 reflog 的引用# 结果:输出引用名称,不改变 reflog 或引用git reflog list
# 用途:检查指定引用是否拥有 reflog# 结果:存在时返回退出状态 0,否则返回非零状态;不输出 reflog 内容git reflog exists <ref># 示例 1:检查本地 main 分支是否拥有 refloggit reflog exists refs/heads/main
# 用途:检查 reflog 中记录的旧位置# 条件:指定记录尚未过期,且其指向的 Git 对象仍然存在# 结果:显示该位置对应的提交及其补丁,不移动任何引用git show <ref>@{<specifier>}# 示例 1:检查 HEAD 在两次移动之前所指向的提交git show HEAD@{2}
# 用途:将 reflog 中找到的提交保留为新分支# 条件:指定记录及其提交仍然存在;<new-branch> 尚不存在# 结果:创建指向该提交的新分支,不切换分支,也不移动现有引用git branch <new-branch> <ref>@{<specifier>}# 示例 1:将 HEAD 在两次移动之前所指向的提交保留为 rescue/examplegit branch rescue/example HEAD@{2}reflog 不是永久备份。默认情况下,可从当前引用到达的记录由 gc.reflogExpire 控制,默认保留 90 天;无法从当前引用到达的记录由 gc.reflogExpireUnreachable 控制,默认保留 30 天。实际期限可以通过配置修改;记录过期后,相应提交若也不再被其他引用或 reflog 保护,之后可能被垃圾回收删除。日常查看与恢复不需要直接执行 reflog expire、delete 或 drop。
bundle:打包仓库历史
bundle 将引用及其可达 Git 对象写入单个文件,可在没有网络连接的环境中传输或备份仓库历史。它不包含工作目录、暂存区、stash、仓库配置、hooks 或 Git LFS 的实际对象。
本节使用的命令行参数:
| 参数 | 含义 |
|---|---|
--all | 包含所有引用 |
# 用途:创建包含所有引用及其可达对象的自包含 bundle# 条件:执行命令所在的工作区可以是 clean 或 unclean# 结果:将当前仓库可由所有引用到达的历史写入 <file>,不包含未提交状态和仓库配置git bundle create <file> --all# 示例 1:将当前仓库历史写入 project.bundlegit bundle create project.bundle --all
# 用途:验证 bundle 格式及其前置提交# 条件:在一个 Git 仓库中执行;验证增量 bundle 时,应在接收方仓库中执行# 结果:检查文件完整性,并列出当前仓库缺少的前置提交;不导入任何对象git bundle verify <file># 示例 1:验证 project.bundlegit bundle verify project.bundle
# 用途:从自包含 bundle 创建新仓库# 条件:<file> 必须是没有缺失前置提交的 bundle,<directory> 不能是已有的非空目录# 结果:在 <directory> 中创建包含 bundle 历史的新仓库git clone <file> <directory># 示例 1:从 project.bundle 创建 project/ 仓库git clone project.bundle project
# 用途:查看 bundle 提供的引用# 结果:显示其中可供读取的引用及其 commit ID,不导入任何对象git ls-remote <file># 示例 1:查看 project.bundle 提供的引用git ls-remote project.bundle
# 用途:从 bundle 获取指定引用# 条件:在接收方仓库中执行;该仓库必须具备 bundle 要求的前置提交# 结果:导入所需对象,并按引用规范更新本地引用git fetch <file> <source-ref>:<destination-ref># 示例 1:将 project.bundle 的 main 分支获取为 bundle/main 远程跟踪引用git fetch project.bundle refs/heads/main:refs/remotes/bundle/mainarchive:归档版本文件
archive 从指定提交、标签或树对象生成源码包。默认只读取该版本中被跟踪的文件,不包含 .git、未提交修改或未跟踪文件。
本节使用的命令行参数:
| 参数 | 含义 |
|---|---|
--format=<format> | 将归档格式设为 <format> |
--prefix=<prefix>/ | 为归档内的路径添加 <prefix>/ |
-o <output>、--output=<output> | 将归档写入 <output> |
# 用途:将指定版本导出为源码包# 条件:执行命令所在的工作区可以是 clean 或 unclean# 结果:把指定版本中的文件写入 <output>,并为包内路径添加 <prefix>/ 前缀git archive --format=<format> --prefix=<prefix>/ -o <output> <tree-ish># 示例 1:将 v1.2.0 标签导出为 project-1.2.0.tar.gzgit archive --format=tar.gz --prefix=project-1.2.0/ -o project-1.2.0.tar.gz v1.2.0.gitignore 会间接影响归档内容:被忽略且未加入版本库的文件不在提交中,git archive 自然不会导出它们。但 .gitignore 不能排除已经存在于被归档版本中的文件;需要从发布包中排除这类受跟踪路径时,在该版本的 .gitattributes 中设置 export-ignore:
.github/** export-ignoretests/** export-ignorebisect:二分定位引入变化的提交
bisect 用于在一个已知为 good 的提交和一个已知为 bad 的提交之间查找状态发生变化的位置。Git 每次选择一个候选提交;将其标记为 good、bad 或 skip 后,Git 缩小范围并选择下一个候选提交,直至定位边界。
整个过程必须使用同一判定标准,并且目标状态在所选范围内应只从 good 变为 bad 一次。无法可靠判断的提交应标记为 skip;如果这类提交紧邻边界,Git 可能只能给出多个候选提交。
# 用途:指定 bad 与 good 边界并开始二分# 条件:两个边界已经过验证;工作区能够安全切换提交# 结果:记录开始前的 HEAD,并切换到第一个候选提交git bisect start <bad> <good># 示例 1:在当前提交与 v1.0.0 之间开始二分git bisect start HEAD v1.0.0
# 用途:将当前候选提交标记为正常# 条件:bisect 正在进行,且当前提交已确认为 good# 结果:更新 good 边界,并切换到下一个候选提交或报告结果git bisect good
# 用途:将当前候选提交标记为异常# 条件:bisect 正在进行,且当前提交已确认为 bad# 结果:更新 bad 边界,并切换到下一个候选提交或报告结果git bisect bad
# 用途:跳过当前无法可靠判断的候选提交# 条件:bisect 正在进行,且当前提交不能归类为 good 或 bad# 结果:选择其他候选提交;边界附近存在 skip 时可能无法确定唯一结果git bisect skip
# 用途:结束二分# 条件:bisect 正在进行# 结果:清除二分状态,并恢复开始二分前所在的分支或提交位置git bisect reset能够用命令或脚本稳定判断状态时,git bisect run 可以自动测试每个候选提交:
| 测试命令的退出状态 | 判定 |
|---|---|
0 | good |
1–127,但不含 125 | bad |
125 | skip |
| 其他值 | 中止二分 |
# 用途:用指定命令自动测试候选提交# 条件:bisect 正在进行;命令能够按约定返回稳定的退出状态# 结果:反复测试并标记候选提交,直至定位边界或中止二分git bisect run <command> [<argument>...]# 示例 1:用仓库外的脚本自动测试候选提交git bisect start HEAD v1.0.0git bisect run ../test-regression.shgit bisect reset测试命令必须能在各候选提交中运行。只有目标状态出现时才应返回 bad;与目标无关的构建失败或环境问题应返回 125。
# 用途:输出当前二分过程的操作记录# 条件:bisect 正在进行# 结果:输出可供检查、保存或 replay 使用的记录,不改变二分状态git bisect log# 示例 1:将记录保存到仓库外的文件git bisect log > ../bisect.log
# 用途:从已有记录恢复二分进度# 条件:<log-file> 是 git bisect log 生成的有效记录;工作区能够安全切换提交# 结果:重放记录中的 start、good、bad 和 skip 操作git bisect replay <log-file># 示例 1:从保存的记录恢复二分进度git bisect resetgit bisect replay ../bisect.logmaintenance:维护仓库数据
maintenance 用于整理不断积累的提交、对象、pack 文件和引用等仓库数据,以缩短历史遍历、获取对象和读取引用所需的时间。它不以修改工作目录中的项目文件为目的,执行时工作区可以是 clean 或 unclean。
长期维护大型或频繁更新的仓库时,通常不需要逐项选择维护任务。最直接的入口是 git maintenance start:它将当前仓库加入用户级维护名单,并建立一个供所有已登记仓库共用的后台调度器。此前没有设置维护策略时,Git 同时采用 incremental 策略,其实际调度如下:
| 频率 | 自动执行的任务 | 实际作用 |
|---|---|---|
| 每小时 | commit-graph、prefetch | 更新提交图;把远程对象预取到 refs/prefetch/,但不移动普通远程跟踪分支 |
| 每天 | loose-objects、incremental-repack | 分批打包松散对象,并逐步合并较小的 pack 文件 |
| 每周 | pack-refs | 整理松散引用,加快大量引用的遍历 |
该策略不调度全面的 gc。prefetch 会访问远程仓库,但不会更新普通远程跟踪分支或标签;之后执行常规 fetch 时,需要传输的对象通常会更少。
本节使用的命令行参数:
| 参数 | 含义 |
|---|---|
--task=<task> | 只运行指定任务;可以重复使用,并按给定顺序执行 |
--auto | 仅在仓库状态达到相应任务的触发阈值时运行 |
--scheduler=<scheduler> | 为 start 指定 auto、crontab、systemd-timer、launchctl 或 schtasks |
--global | 用于 git config 时,读取当前用户的全局配置 |
--get-all <name> | 输出配置项 <name> 的全部取值 |
--unset <name> | 删除配置项 <name> |
# 用途:让当前仓库开始接受定时维护# 条件:执行命令所在的工作区可以是 clean 或 unclean;操作系统中存在可用调度器# 结果:登记当前仓库,未配置策略时设为 incremental,关闭普通 Git 命令触发的自动维护,并创建或更新用户级后台调度git maintenance start [--scheduler=<scheduler>]# 示例 1:让 Git 根据操作系统自动选择调度器git maintenance start --scheduler=auto
# 用途:在不改动现有调度器的情况下,将另一个仓库加入维护名单# 条件:在需要加入的仓库中执行;工作区可以是 clean 或 unclean# 结果:登记当前仓库并完成与 start 相同的仓库配置,但不创建或启动调度器git maintenance register# 示例 1:调度器已经由其他仓库启动,将相邻的 project-b 加入同一维护名单cd ../project-bgit maintenance register
# 用途:查看已登记的仓库# 结果:输出当前用户全局配置中的所有 maintenance.repo 值git config --global --get-all maintenance.repo
# 用途:只停止当前仓库的定时维护# 条件:当前仓库已经登记;工作区可以是 clean 或 unclean# 结果:从维护名单中移除当前仓库,其他仓库和调度器保持不变;maintenance.auto=false 仍然保留git maintenance unregister# 可选:恢复普通 Git 命令按需触发自动维护的默认行为git config --unset maintenance.auto
# 用途:暂停所有已登记仓库的定时维护# 条件:工作区可以是 clean 或 unclean# 结果:停止并移除共用的用户级调度器,但保留维护名单;以后执行 start 可以继续处理这些仓库git maintenance stop
# 用途:针对明确问题手动运行指定任务# 条件:执行命令所在的工作区可以是 clean 或 unclean;每个 <task> 都必须是受支持的任务名称# 结果:仅按参数出现顺序执行指定任务,不运行其他任务git maintenance run --task=<task> [--task=<task>...]# 示例 1:导入大量提交后,立即更新 commit-graphgit maintenance run --task=commit-graph# 示例 2:仓库积累了大量松散对象和较小 pack 文件,执行增量整理git maintenance run --task=loose-objects --task=incremental-repack# 示例 3:需要一次全面整理时单独运行 gc;不要与 loose-objects 放在同一次维护中git maintenance run --task=gc
# 用途:仅在仓库达到相应维护阈值时运行任务# 条件:执行命令所在的工作区可以是 clean 或 unclean# 结果:达到阈值时执行维护,否则不改变仓库数据git maintenance run --auto全面 gc 可能耗时较长,也可能清理已经超过保留期限且无法到达的数据。启用 start 后通常无需再手动运行上述任务;显式指定 --task 主要用于立即处理已经确认的仓库数据问题。
已推送分支变基到最新 main 后,本地与远端分叉
这一节处理一个明确场景:feature/example 上的提交已经推送,随后本地分支变基到更新后的 origin/main。变基只改写本地历史,因此本地分支与远端旧历史发生分叉;此时普通 push 会被拒绝,pull 也不会直接解决问题。
问题如何产生
- 从
main创建feature/example,完成两个提交C、D。 - 推送分支;此时
feature/example与origin/feature/example都指向D。 main随后增加 29 个提交。- 在
feature/example上执行git fetch origin和git rebase origin/main。Git 在新基线后重新创建C′、D′,本地分支改为指向D′;远端仍然指向旧的D。
变基后的提交图
图中 M* 折叠表示 main 新增的 29 个提交。变基只改写了本地:远端引用停在 C、D,本地分支则经过 M* 后指向重新创建的 C′、D′:
提交图同时解释了 ahead 与 behind。查看短格式状态:
git status --short --branch会得到:
## feature/example...origin/feature/example [ahead 31, behind 2]ahead 和 behind 按提交的可达关系计数,不比较文件内容:
ahead 31:M*代表的 29 个提交,加上本地的C′、D′。behind 2:远端独有的旧提交C、D。
一般地,如果 main 在分叉后增加了 N 个提交,功能分支上已有的 K 个提交又全部被变基,那么在没有其他分叉的前提下会显示 ahead N + K、behind K。这里的 behind 2 说明提交图确实分叉了,但不能单凭这个数字判断远端有人增加了工作。
此时执行 push
git push origin feature/example普通推送会被拒绝,输出通常包含:
! [rejected] feature/example -> feature/example (non-fast-forward)error: failed to push some refs to '<remote-url>'普通推送只允许 fast-forward:推送前的远端分支尖端必须是待推送提交的祖先。这里的远端尖端是 D,而本地尖端 D′ 位于另一条历史上,D 不是 D′ 的祖先。即使 D 与 D′ 最终引入相同的文件修改,这项检查仍然不会通过。
此时执行 pull
git pullpull 会先获取远端状态,再尝试把 origin/feature/example 整合进当前分支。因为两侧已经分叉,之后的现象取决于 pull 策略:
| pull 策略 | 现象 |
|---|---|
| 未配置 merge 或 rebase | Git 停止并提示 Need to specify how to reconcile divergent branches |
pull.ff=only 或 git pull --ff-only | Git 停止并提示 Not possible to fast-forward |
| merge | Git 尝试合并 D 与 D′;可能产生冲突,成功时会留下同时连接新旧历史的合并提交 |
| rebase | Git 以旧的 origin/feature/example 为基线再次重放本地独有提交;可能发生冲突或得到非预期历史 |
配置了 pull.rebase=true 时,git pull 走最后一条路径。发生冲突时,输出通常包含 CONFLICT 和 could not apply,仓库会停在尚未完成的 rebase 中。
这些行为都在尝试整合远端旧历史,而本例真正需要的是用 C′、D′ 替换 C、D。因此,pull 即使能够完成,也不是这个场景的正确处置。
如果误执行的 pull 正停在冲突中,先按实际进行状态中止:
# pull 使用 rebase 时git rebase --abort
# pull 使用 merge 时git merge --abort如果 pull 在开始整合前已经报错退出,不需要执行中止命令;如果它已经完成,应先通过 reflog 找回 pull 之前的分支位置,再继续下面的步骤。
解决方法
正确目标是让远端 feature/example 从旧提交 D 改为指向变基后的 D′。前提是这个分支允许改写,而且远端没有其他人新增的工作。
先更新远程跟踪引用并检查远端独有提交:
git fetch origin
# 查看分叉后的完整提交图git log --graph --oneline --decorate --boundary \ feature/example...origin/feature/example
# 只查看远端有、本地没有的提交git log --oneline \ feature/example..origin/feature/example
# 检查这些远端提交是否存在补丁等价的本地版本git cherry -v \ feature/example origin/feature/example本例中,远端独有提交应当只有变基前的 C、D;git cherry 通常会在它们前面标记 -,表示本地存在补丁等价的 C′、D′。- 只是辅助证据,仍需根据提交 ID、提交说明和协作情况确认它们确实是准备替换的旧提交。
确认无误后,用带保护的强制推送更新远端:
# 用途:以变基后的本地历史替换远端旧历史# 条件:已经 fetch 并确认远端只有变基前的旧提交;该分支允许改写# 结果:保护检查通过时,将 origin/feature/example 更新到本地 feature/examplegit push \ --force-with-lease \ --force-if-includes \ origin feature/example这条推送命令包含两层保护:--force-with-lease 检查远端分支是否仍处于预期位置,--force-if-includes 检查该远端位置是否真正进入过本地分支历史。理解这两项检查前,先区分本例中的三个引用:
| 位置 | 指向 | 含义 |
|---|---|---|
服务端 feature/example | D | 推送真正准备改动的远端分支 |
本地 origin/feature/example | D | 最近一次 fetch 记录的远端位置 |
本地 feature/example | D′ | 变基后准备推送的新历史 |
--force-with-lease 可以理解为:“我只同意覆盖自己预期中的那个远端版本。”
这里没有显式填写期望值,因此 Git 使用本地 origin/feature/example 的值 D 作为期望。服务端在更新引用时进行原子比较:
- 服务端仍指向
D:说明远端分支没有偏离本地记录的状态,检查通过,可以继续尝试把它更新为D′。 - 服务端已经指向别人新推送的
X:X与本地记录的D不同,检查失败,远端保持不变。
这项检查不比较文件内容,也不会判断 C、D 是否真的是旧副本;这些结论必须在前面的历史检查中确认。
--force-with-lease 仍有一个缺口:编辑器可能在别人推送 X 后自动执行 fetch,把本地 origin/feature/example 也更新为 X。此时服务端和本地记录再次相等,单独使用 lease 就会通过,尽管自己可能从未看过或处理过 X。
--force-if-includes 用来检查这个缺口。它要求本地 feature/example 的引用日志表明,当前远程跟踪尖端曾经被包含在该分支的历史中,而不只是被 fetch 记录过:
- 本例的
D曾是变基前feature/example的尖端,分支引用日志保留了这个位置,因此检查通过。 - 如果
X只是被后台fetch到origin/feature/example,本地功能分支从未包含X,检查失败。 - 如果已经有意把
X整合进本地分支,祖先关系和引用日志能够证明这一点,检查才会通过。
组合后的结果如下:
| 情况 | --force-with-lease | --force-if-includes | 结果 |
|---|---|---|---|
远端仍是旧提交 D | 通过 | 通过 | 允许更新 |
别人推送了 X,本地尚未 fetch | 失败 | 无需继续 | 拒绝更新 |
后台已经 fetch 到 X,但本地未整合 X | 通过 | 失败 | 拒绝更新 |
本地已经有意整合 X | 通过 | 通过 | 允许更新 |
“允许更新”只表示这两项保护检查通过,不代表 Git 已经替人判断了业务意图。任一检查失败时,都应重新 fetch 和检查,不要改用无条件的 --force。
推送成功后再次确认:
git fetch origingit status --short --branch预期不再出现 ahead 或 behind:
## feature/example...origin/feature/example显式指定 lease 的期望值
前一条命令的 lease 期望值来自 origin/feature/example,而该引用可能被之后的 fetch 更新。若希望把推送条件固定为“服务端分支必须仍指向刚刚核对过的 D”,可以显式指定 D 的 commit ID:
# 输出并人工核对远端尖端;本例应当是变基前的旧提交 Dgit rev-parse origin/feature/example
# 将上一步输出的完整 commit ID 填入 <expected-remote-tip>git push \ --force-with-lease=refs/heads/feature/example:<expected-remote-tip> \ origin feature/example:feature/example这种形式直接比较服务端 refs/heads/feature/example 与指定的 commit ID,不再依赖可能变化的远程跟踪引用。两者相等时才接受更新,否则远端保持不变。
使用 --force-with-lease=<ref>:<expect> 时,--force-if-includes 不起作用,不应同时使用。若服务端检查失败,应重新获取并检查远端历史,而不是改用 --force。
远端存在其他来源的新提交时
如果 git log feature/example..origin/feature/example 出现 C、D 之外的不认识提交,或者 git cherry 输出无法解释的 +,停止强制推送。此时不能再假设远端只有等待替换的旧历史。
接下来的处理取决于希望保留哪种历史:
| 目标 | 处理方式 |
|---|---|
| 完整保留现有远端历史 | 合并 origin/feature/example,解决冲突后普通推送 |
| 保持变基后的线性历史 | 与提交作者协调,只把确认需要的新提交 cherry-pick 到 D′ 后,再安全强推 |
| 分支受保护或不允许改写 | 将变基后的历史推送为新分支,通过合并请求处理 |
选择完整保留远端历史时:
# 条件:工作区没有会妨碍合并的未提交修改git merge origin/feature/examplegit push origin feature/example合并提交会同时包含本地与远端尖端,因此普通推送能够 fast-forward 远端。代价是变基前后的两条功能分支历史都会保留。
如果只需要远端新增的个别提交,应明确选择它们:
git cherry-pick <remote-new-commit>...完成冲突处理和测试后,重新执行本节的远端检查,再使用带保护的强制推送。不要机械执行 git rebase origin/feature/example:它会把旧的远端功能分支历史选作新基线,并可能把 main 的那批提交也作为本地独有提交重放,偏离最初把功能分支建立在最新 main 上的目的。
如果分支不允许改写,可以保留当前变基结果并推送为新分支:
git switch -c feature/example-rebasedgit push --set-upstream origin feature/example-rebased减少此类分叉
-
创建功能分支前先更新远程跟踪引用,并从最新
origin/main开始:Terminal window git fetch origingit switch -c feature/example origin/main -
缩短功能分支的生命周期,减少为了追赶
main而改写已推送历史的需要。 -
个人分支允许改写时,把“变基到最新
main”和“带 lease 的强制推送”视为同一次操作;变基后不要先执行 Pull 或 Sync。 -
已经共享且不希望改写的分支,使用 merge 引入最新
main,保留原有提交 ID:Terminal window git fetch origingit merge origin/maingit push origin feature/example -
托管平台禁止强制推送时,遵守分支保护规则;需要线性新历史时推送到新分支,再通过合并请求处理。