技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTGIT

图解 Git 原理与日常实用指南

所属馆藏
Git
文件格式
Markdown
原始路径
git/03-图解git原理与日常实用指南
本文目录24 个章节
  1. 中央式版本控制系统(VCS)
  2. 分布式版本控制系统(DVCS)
  3. Git 最基本的工作模型
  4. 团队工作基本模型
  5. Feature Branching:常见的功能分支工作流
  6. HEAD:当前检出位置的特殊引用
  7. branch:指向提交的可移动引用
  8. 引用的本质
  9. merge:合并
  10. rebase:给提交序列重新设置基础点
  11. stash:临时保存工作目录改动
  12. reflog:引用变化记录
  13. log:查看已提交内容
  14. diff:查看未提交内容
  15. 修改最近一次提交:commit --amend
  16. 错误不是最新的提交:交互式 rebase
  17. reset --hard:回退当前分支并丢弃工作区改动
  18. 用交互式 rebase 删除历史提交
  19. 用 rebase --onto 移除一段提交
  20. 错误内容只在自己的功能分支
  21. 问题内容已经进入共享分支
  22. 切换分支或进入 detached HEAD
  23. 恢复文件
  24. 参考资料

图解 Git 原理与日常实用指南

Category(分类): git Status: 未知

本文尽量保留原文从版本控制、工作区/暂存区/版本库、团队协作、分支、HEAD、引用、merge、rebase、stash、reflog、diff、amend、reset 和 checkout 等方面的内容,并根据当前 Git 官方文档补充和修正命令语义。

文中历史截图仍保留,但命令示例优先采用现代 Git 推荐的 switchrestore--force-with-lease。不同 Git 版本、配置、托管平台和默认分支名称可能导致输出略有差异。

从了解版本控制系统开始

所谓版本控制,就是在文件修改的历程中保存版本历史,方便查看差异、回退、比较和协作。一个完整的版本控制流程通常包含:

  1. 记录文件或项目的历史版本;
  2. 由开发者主动创建提交(commit);
  3. 在需要时与远程仓库同步并协作。

远程仓库不是版本控制的必要条件:Git 可以完全在本地使用,远程仓库主要用于备份、共享、代码评审和团队协作。

中央式版本控制系统(VCS)

中央式版本控制系统通常把主要版本库放在服务器上。典型工作模型是:

  1. 主工程师在公司服务器创建中央仓库并提交项目框架;
  2. 其他人从服务器获取代码;
  3. 每个人并行开发,开发完成后把改动提交到中央服务器;
  4. 其他人再从中央服务器同步最新版本。

中央式版本控制系统工作模型

中央式系统的优点是权限、版本库和协作入口集中,管理方式较直观;缺点是提交、查看历史等操作往往依赖中央服务器,服务器或网络不可用时工作会受到影响,分支和离线提交能力也通常不如分布式系统灵活。

分布式版本控制系统(DVCS)

分布式版本控制系统除了可以有远程仓库外,团队成员的机器上通常还拥有一份本地仓库。每个人可以在本地提交代码、查看历史、切换分支和进行合并,不必每一步都依赖网络。

分布式版本控制系统工作模型

典型工作模型是:

  1. 主工程师在本地创建项目并提交初始版本;
  2. 在服务器上创建远程仓库,并把本地提交推送到远程仓库;
  3. 其他人克隆远程仓库,得到工作区和本地 Git 仓库;
  4. 每个人可以把小改动逐步提交到本地,不必每个中间提交都是完整功能;
  5. 功能完成后,把相关提交推送到远程仓库;
  6. 其他人通过 fetchpull 或代码托管平台的合并请求同步和审查这些提交。

默认 git clone 通常会获取完整历史,但 Git 也支持浅克隆、部分克隆和 blob 过滤等方式,因此“克隆一定得到所有对象”是入门场景的简化说法。

分布式版本控制系统的优缺点

优点

  • 大多数操作在本地进行,查看历史、创建提交、切换分支和比较差异速度较快;
  • 网络暂时不可用时仍可以提交和整理本地历史;
  • 每个克隆通常都是独立的本地仓库,远程服务器故障时仍可能保留完整历史;
  • 可以通过细粒度提交、分支和代码评审组织协作;
  • Git 的对象模型和引用机制支持合并、重放、回退与恢复。

缺点

  • 初次克隆完整历史可能耗时较长并占用较多存储;
  • Git 的分支、暂存区、引用和重写历史等概念比简单的中央式工作流更复杂;
  • 本地仓库和远程仓库可能产生分叉,需要理解 fetch、合并、rebase 和冲突处理;
  • 权限、分支保护、提交签名和敏感信息治理仍需要远程平台或团队流程配合。

继续深入 Git 原理

假设已经安装好 Git,并把代码克隆到了本地。Git 的安装和克隆可以参考 Git 官方安装文档git-clone 官方文档

Git 最基本的工作模型

首先理解三个常用概念:

  • 工作区(working tree):当前在电脑文件系统中看到、编辑的项目目录;
  • 暂存区(index/staging area):准备下一次提交的快照。普通仓库通常位于 .git/index,但 Git 也允许通过环境变量和其他仓库布局改变其位置;
  • 本地版本库(repository):保存 Git 对象、引用、配置和日志的数据库,普通仓库的管理数据通常位于 .git 中。

工作区、暂存区和版本库

暂存区不是“自动保存所有改动的缓存”,而是下一次 git commit 将要写入的内容。工作区文件在 git add 之后被加入暂存区;之后如果再次编辑文件,工作区和暂存区又会产生差异,需要再次 git add 才会更新下一次提交的内容。

一个文件从未跟踪到提交

  1. 新建 test.txt 并查看状态。因为它还没有被纳入当前提交,Git 会把它显示为 Untracked files

未跟踪文件状态

  1. 使用 git add 把文件当前内容放入暂存区。对于新文件,这一步也意味着 Git 开始在提交中跟踪它。
git add test.txt
# 或者有选择地暂存补丁
git add -p test.txt

执行 git add 后的状态

git status 中通常会显示 Changes to be committed,而不是原文截图中的拼写错误 commited。需要注意:Git 暂存的是文件在这一时刻的快照,不是一个“永远跟随工作区变化”的标记。

stage 表示把改动收集起来以待提交,staging area 表示暂存区,staged 表示已经进入下一次提交候选快照。

  1. 文件进入暂存区后,可以创建提交:
git commit -m "添加 test.txt"

创建提交

不使用 -m 时,Git 会打开编辑器填写提交信息。提交完成后,可以使用下面的命令查看历史:

git log
# 更适合日常浏览的简洁图形视图
git log --oneline --graph --decorate --all
  1. 再次修改已经被跟踪的文件,运行:
git status

它通常会显示 Changes not staged for commit,表示工作区有改动,但暂存区仍然是上一次 git add 的内容。

已跟踪文件产生未暂存改动

再次执行 git addgit commit,就会产生新的提交:

git add test.txt
git commit -m "更新 test.txt"
git log --oneline

查看多条提交记录

  1. 最后可以把本地分支推送到远程仓库:
# 第一次推送本地新分支时,建立上游跟踪关系
git push -u origin main
# 以后可以直接使用
git push

推送本地提交到远程仓库

git push 不等于把“本地所有 commit 无条件覆盖到远端”。它按照 refspec 更新指定的远程引用,并传输远端缺少的对象;默认情况下,分支更新通常必须是快进更新,Git 会拒绝可能丢失远端提交的非快进推送。

团队工作基本模型

一种简单的团队流程是:

  1. 同事在自己的本地分支提交代码,并推送到远程仓库;
  2. 你先用 git fetch 获取远程引用和对象;
  3. 通过代码评审、git mergegit rebasegit pull 把变更整合到本地;
  4. 解决冲突、运行检查后再推送。

可以把常见命令区分为:

# 只获取远程更新,不修改当前工作分支
git fetch origin

# 查看远程分支的提交
git log --oneline --graph --decorate HEAD..origin/main

# 把远程 main 合并到当前分支
git merge origin/main

# 或者把当前分支提交重放到远程 main 之后(会改写当前分支提交)
git rebase origin/main

原文使用的 git pull 确实常被描述为 fetch 加整合操作,但现代 Git 中 pull 的第二步取决于配置和命令参数:默认可能是 merge,也可以用 git pull --rebase 选择 rebase,用 git pull --ff-only 只允许快进。团队应统一策略,避免每个人使用不同的默认行为。

# 明确采用 rebase
git pull --rebase origin main

# 只接受快进,有分叉就停止,让开发者显式处理
git pull --ff-only origin main

为什么 push 有时会失败?

假设远程 origin/main 已经有了本地没有的提交。如果本地直接推送会让远程分支指针后退,Git 会以 non-fast-forward 拒绝推送,防止远程提交被默认覆盖。

错误的简化说法是“push 用本地提交记录覆盖远程提交”。更准确的说法是:

  • push 请求包含一个或多个 ref 更新,例如 refs/heads/main
  • Git 会把远端缺少且允许传输的对象发送给远端;
  • 远端检查权限、钩子和更新规则;
  • 对普通分支,远端通常只接受旧值是新值祖先的快进更新;
  • 如果本地和远端都产生了新提交,需要先合并或 rebase,再推送。
git fetch origin
git merge origin/main
# 或:git rebase origin/main
git push origin main

若远程仓库使用了保护分支、必须评审或必须通过 CI,直接 push 仍可能被服务器拒绝,这是协作策略而不是 Git 本地错误。

Feature Branching:常见的功能分支工作流

原文称 Feature Branching 是“最流行的工作流”,这属于历史经验,不是 Git 的强制模式。Git 团队还可能使用 trunk-based development、GitHub Flow、GitLab Flow 或其他分支策略。

常见的功能分支流程是:

  1. 为新功能或 bug 修复创建独立分支;
  2. 在分支上提交小而清晰的变更;
  3. 推送分支并创建 Pull Request/Merge Request;
  4. 通过评审、测试和 CI 后合并到默认分支;
  5. 删除不再使用的本地和远程分支。

如今新仓库的默认分支经常叫 main,历史项目也可能叫 master;分支名不是 Git 固定规定的。

功能分支工作流

创建并切换到功能分支,现代 Git 推荐:

git switch main
git switch -c feature/login

旧版本和历史文章中常见的等价写法是:

git checkout -b feature/login

功能完成后:

git add .
git commit -m "完成登录功能"
git push -u origin feature/login

在合并完成后删除分支:

# 删除已合并的本地分支
git branch -d feature/login

# 删除远程分支
git push origin --delete feature/login

原文把删除远程分支写成了错误的命令;origin 是远程名称,不是 Git 子命令。删除服务器上的远程分支应使用:

git push origin --delete feature/login

如果服务器分支已经删除,但本地仍显示旧的远程跟踪引用,可以单独清理本地引用;这不会删除服务器上的分支:

git branch --delete --remote origin/feature/login

功能分支的优势

  • 代码分享:功能完成后可以在开发分支上评审,再合并到 main/master
  • 一人多任务:可以先把当前工作提交到功能分支,切换到另一个分支处理紧急任务;如果改动尚未适合提交,也可以使用 stash,但不要把长期未完成的代码直接混入公共分支;
  • 隔离风险:未完成或未通过测试的功能不会直接进入受保护的默认分支;
  • 方便回滚:功能可以通过反向提交、关闭合并请求或回退合并提交进行处理。

HEAD、branch、引用的本质以及 push 的本质

HEAD:当前检出位置的特殊引用

HEAD 是 Git 中的特殊引用,用来表示当前检出位置:

  • 正常情况下,HEAD 是一个符号引用(symbolic ref),例如 ref: refs/heads/main,间接指向当前分支的最新提交;
  • 处于 detached HEAD 状态时,HEAD 直接指向某个提交,不再附着在分支上;
  • 新仓库尚未有第一次提交时,HEAD 可能指向一个尚未诞生的分支(unborn branch)。

HEAD 指向当前分支

因此,“当前 commit 在哪里,HEAD 就在哪里”是便于入门的说法;更准确地说,HEAD 表示当前检出位置,可能是一个分支、一个提交,甚至是尚未有提交的分支名。

可以用以下命令查看:

git status
git symbolic-ref --short HEAD
git rev-parse --short HEAD

branch:指向提交的可移动引用

分支本质上是一个可移动的引用,通常指向某个提交。它不是一条独立复制的“代码路径”,也不包含一份文件副本;提交之间通过父提交形成有向无环图,分支只是这个图上的一个命名指针。

分支引用和提交图

如果 HEAD 附着在 main 上,在 main 上创建新提交时,main 分支指针会前移;如果 HEAD 处于 detached 状态,新提交只会移动 HEAD,除非随后创建分支,否则提交可能变成难以找到的悬空提交。

提交后分支指针移动

合并提交可能有两个或更多父提交。因此,分支能沿父提交回溯到一组可达提交,但不能简单说成“branch 包含从初始 commit 到它的所有平等路径”。不同父路径只是提交图中的不同祖先关系,合并基线、第一父链和拓扑顺序仍然会影响日志展示与发布流程。

合并后的提交图

创建、切换和删除分支

# 查看本地分支
git branch

# 从当前提交创建分支,但不切换
git branch feature/login

# 切换到已有分支
git switch feature/login

# 创建并切换
git switch -c feature/login

历史写法 git checkout 名称git checkout -b 名称 仍可用,但 Git 2.23 之后把“切换分支”和“恢复文件”拆成了更明确的 switchrestore

如果在功能分支上提交,HEAD 和该功能分支会一起前移;切回 main 后再提交,则会产生分叉:

删除分支:

# 安全删除:通常要求分支已合并到其上游分支或当前分支
git branch -d feature/login

# 强制删除:即使 Git 判断仍有未合并提交也删除引用
git branch -D feature/login

注意:

  1. 不能删除当前正检出的分支;先切换到其他分支;
  2. 删除分支通常只是删除一个引用,不会立即删除提交对象;
  3. 没有任何引用可达的对象会在 reflog 过期并经过对象清理后被回收,不是删除分支后立刻消失;
  4. -d 检查的是合并关系和上游配置,不是固定检查是否合并到名为 master 的分支;
  5. 删除远程分支不会自动删除其他人的本地分支,其他人还需要清理对应的 remote-tracking ref。

引用的本质

引用(ref)是 Git 为提交、标签或其他对象提供的可读名称。常见引用包括:

refs/heads/main             # 本地分支
refs/remotes/origin/main    # 远程跟踪分支
refs/tags/v1.0.0            # 标签
HEAD                        # 特殊引用,可能是符号引用

引用最终解析到对象 ID。历史文章经常使用 40 位 SHA-1 示例:

c08de9a4d8771144cd23986f9f76c4ed729e69b0

但现代 Git 也支持 SHA-256 对象格式,不能把 SHA-1 或固定 40 个十六进制字符当作永远不变的 Git 规则。在具体仓库中,使用 git rev-parse、短哈希或完整对象 ID 解析结果更稳妥。SHA-256 仓库与传统 SHA-1 仓库之间仍存在托管平台、协议和工具兼容性限制,不能仅凭更换哈希算法就假设所有远程仓库都能直接互通。

普通仓库中的引用历史上常以 .git/refs/ 下的文本文件保存,部分引用也会集中写在 .git/packed-refs;现代 Git 还可能使用其他引用存储后端。因此“每一个引用永远都是一个单独文本文件”是便于理解的历史实现描述,不是接口保证。

可以查看引用:

git show-ref
git for-each-ref
git rev-parse --symbolic-full-name HEAD

Git 的核心对象主要包括:

  • blob:文件内容,不直接保存文件名;
  • tree:目录快照,记录文件名、模式和 blob/tree 对象;
  • commit:提交说明、作者、提交者、时间、根 tree 和一个或多个父提交;
  • tag object:可选的带注释标签对象。

分支移动的只是引用,提交对象本身是不可变对象;修改提交内容会生成新的提交对象,而不是原地改变旧提交。

push 的本质:更新远程引用

git push 的核心不是“把分支文件夹上传”,而是根据 refspec 请求远程仓库更新引用,并传送远程缺少的对象。

# 把本地 main 推送到远程 origin 的 main
git push origin main

# 明确写出 refspec
git push origin refs/heads/main:refs/heads/main

# 推送当前分支,并记录上游
git push -u origin feature/login

远程跟踪分支 origin/main 是本地仓库对远程 main 上次已知状态的记录,不会因为服务器上有人推送就自动更新,必须通过 git fetchgit pull 刷新。

关于原文中的几个说法,需要修正:

  1. git push 不保证只能推送“从远程 clone 或 pull 的分支”。本地新分支可以通过 git push -u origin 新分支 首次推送;
  2. git push 默认更新当前配置的上游,但没有上游或配置不允许时需要显式指定;
  3. 远程仓库的 HEAD 通常是一个符号引用,指向远程默认分支。默认分支可能是 mainmaster 或其他名称,不是永远的 master
  4. 推送普通分支不会让远程默认分支自动改指向新分支。若推送的是默认分支,默认分支本身前移后,远程 HEAD 解析到的提交自然也会改变;
  5. git branch -r 可以查看本地远程跟踪分支,git remote show origin 可以查看远程信息和默认分支提示。git br -r 只有在用户自己配置了 br 别名时才有效。

开启 Git 操作之旅

merge:合并

在当前分支执行:

git switch main
git merge feature/login

Git 会找到当前分支和目标分支的合并基点,比较两个分支相对于合并基点的变化,并把结果整合到当前分支。

merge 合并提交

如果当前分支和目标分支已经分叉,默认会创建一个包含两个父提交的 merge commit;如果目标分支只是当前分支的后代,Git 可以直接把当前分支指针移动到目标提交,这叫 fast-forward,不会创建新的合并提交。

原文图例中把提交 5、6 应用到提交 4 并生成提交 7,表达的是一次典型三方合并;实际合并结果还取决于共同祖先、文件内容、重命名检测和冲突解决方式。

merge 的特殊情况

(1)merge 冲突

两个分支修改相同文件的同一区域时容易产生冲突,但冲突不只来自“同一行”:重命名/删除、文件/目录冲突、二进制文件、add/add 等也可能冲突。Git 会把冲突标记写入工作区,等待开发者决定最终内容。

git merge feature/login
# 编辑冲突文件,删除 <<<<<<<、=======、>>>>>>> 标记

git add <已解决的文>
git merge --continue
# 或者创建合并提交:git commit

想放弃正在进行的合并:

git merge --abort

--abort 主要用于仍处于合并状态时;如果合并前工作区本来就有未提交改动,Git 不一定能够完全恢复到合并前的状态,所以合并前应保持工作区干净或先妥善保存改动。

(2)当前分支已经领先目标分支

如果目标分支是当前分支的祖先,合并没有新内容,Git 通常会提示:

Already up to date.

当前分支不会移动,也不会创建新提交。

目标分支已被当前分支包含

(3)可以快进(fast-forward)

如果当前分支是目标分支的祖先,Git 可以直接把当前分支指针移动到目标提交:

fast-forward 合并

# 默认允许 fast-forward
git merge feature/login

# 强制创建一个合并提交
git merge --no-ff feature/login

# 只允许 fast-forward,不满足时失败
git merge --ff-only feature/login

--no-ff 是否使用取决于团队对历史和发布记录的偏好,并不是“所有合并都应该使用”或“所有合并都应该避免”。

rebase:给提交序列重新设置基础点

有些团队不喜欢合并提交过多造成的分叉,会在自己的功能分支上使用 rebase,把功能分支上独有的提交重新应用到目标分支最新提交之后:

git switch feature/login
git fetch origin
git rebase origin/main

rebase 重新设置基础点

例如,功能分支原先从提交 2 分叉,并有提交 5、6;主分支后来前进到提交 4。rebase 会把 5、6 的变更重新应用到 4 上,生成新的提交 5'、6',而不是修改原来的 5、6。

rebase 后的线性历史

rebase 完成后,如果功能分支只领先 main,可以这样快进合并:

git switch main
git merge --ff-only feature/login

也可以在功能分支上完成 rebase 后直接推送自己的远程分支。由于 rebase 改写了提交 ID,已经推送过的分支通常需要:

git push --force-with-lease origin feature/login

不要在没有确认影响范围的情况下对公共分支执行 rebase 或 git push --force。共享分支上已经被别人基于的提交被改写后,会给协作者造成额外的恢复和合并问题。

为什么通常在功能分支上 rebase,而不是直接在 main 上 rebase?

rebase 会创建新的提交对象。即使文件内容看起来相同,提交的父提交、时间、元数据或变更身份发生变化,提交 ID 也会变化。因此:

  • 在个人或功能分支上 rebase,影响范围较小;
  • 在已经共享的 main/master 上 rebase,会改写协作者共同依赖的历史;
  • 如果默认分支要求线性历史,可以在合并请求中采用 rebase/fast-forward 策略,而不是让每位开发者直接重写公共分支。

rebase 改写提交身份示意

需要说明的是,rebase 的命令位置是:通常站在“需要被重放的分支”上执行,例如站在 feature/login 上执行 git rebase main;它与站在 main 上执行 git merge feature/login 的工作位置不同。

遇到冲突时:

# 解决冲突后
git add <>
git rebase --continue

# 跳过当前提交
git rebase --skip

# 放弃整个 rebase
git rebase --abort

stash:临时保存工作目录改动

git stash 可以把暂时不想提交的改动保存到本地 stash 栈中,以便切换分支处理其他工作。它不是远程备份,也不是永久版本库;stash 本质上是由特殊引用 refs/stash 保存的一组提交对象。

# 保存已跟踪文件的工作区和暂存区改动
git stash push -m "暂存登录功能进度"

# 查看 stash
git stash list

# 应用最近一条并从 stash 栈删除
git stash pop

# 应用但保留原 stash
git stash apply stash@{0}

# 删除某一条或清空 stash(谨慎使用)
git stash drop stash@{0}
git stash clear

原文中的 git stash save '备注信息' 是历史写法,现在更推荐 git stash push -m '备注信息'。默认 stash 主要处理已跟踪文件的工作区/暂存区改动:

# 连同未跟踪文件一起保存
git stash push -u -m "保存未跟踪文件"

# 连同被 .gitignore 忽略的文件也保存(谨慎使用)
git stash push -a -m "保存全部改动"

应用 stash 可能产生冲突;如果改动很重要,提交到专用临时分支通常比长期依赖 stash 更安全。git stash pop 在应用冲突时一般不会自动删除原 stash,处理前要查看 git status

reflog:引用变化记录

reflog 记录本地仓库中 HEAD、分支等引用曾经指向过哪些位置,可以帮助找回误删分支、错误 reset 或 rebase 前的提交:

git reflog
# 查看某个分支的 reflog
git reflog show feature/login

例如发现误删分支前的提交是 abc1234,可以:

git switch --detach abc1234
git switch -c recovery/login

旧版 Git 中也常见 git checkout abc1234,它会进入 detached HEAD;现代命令建议用 switch --detach 表达意图。

需要注意:

  • reflog 默认是本地记录,不会随 git push 上传到远程仓库;
  • 远程跟踪分支是否保留 reflog 还与本地配置有关;
  • reflog 会按过期策略清理,不能保证永久保存;
  • 如果对象已不可达并被清理,恢复难度会明显增加;
  • 发现误操作后应尽快使用 git reflog 保存对象 ID 或创建恢复分支。

看看我都改了什么

log:查看已提交内容

# 查看提交历史和每次补丁
git log -p

# 查看简要统计
git log --stat

# 查看紧凑的图形历史
git log --oneline --graph --decorate --all

# 查看某次提交及某个路径的改动
git show <commit> -- <path>

# 查看某个文件的历史
git log --follow -- <path>

原文中的 git show 指定commit 指定文件名 需要加 -- 分隔提交和路径,避免路径名被误解析为修订参数。

diff:查看未提交内容

Git 中常用的三个比较面如下:

# 工作区 vs 暂存区:哪些工作区改动还没有 add
git diff

# 暂存区 vs HEAD:下一次 commit 将提交什么
git diff --staged
# --cached 与 --staged 等价

# 工作区+暂存区 vs HEAD:当前已跟踪内容相对 HEAD 的全部改动
git diff HEAD

git diff HEAD 默认不显示未跟踪文件,因为未跟踪文件还没有进入 Git 的对象和索引体系。可以用 git status --short 查看它们;也可以使用 git diff --no-index /dev/null <file> 比较未跟踪文件,但不同平台的 /dev/null 写法需要注意。

刚刚提交的代码发现写错了怎么办?

修改最近一次提交:commit --amend

如果错误只在最近一次、且该提交还没有被别人基于,可以:

# 先修改文件
git add <>
git commit --amend
# 不修改原提交信息时
git commit --amend --no-edit

--amend 会用新的树和父提交生成一个新的提交对象,原提交 ID 通常会改变。已经推送到公共分支的提交不应随意 amend;若必须改写自己的远程分支,应与协作者确认,并优先使用 --force-with-lease

使用 commit --amend 修改最近提交

错误不是最新的提交:交互式 rebase

交互式 rebase 可以重新排序、编辑、拆分、合并或删除一段尚未公开的提交:

# 选取最近两个提交进行编辑
git rebase -i HEAD~2

HEAD~2 表示沿第一父链向前两个提交。HEAD^^ 在简单的线性历史中通常等价于 HEAD~2,但遇到合并提交时,^~ 的含义必须按父提交规则理解:HEAD^1 是第一父提交,HEAD^2 是第二父提交,而 HEAD~2 是沿第一父链走两步。

编辑器中常见操作:

  • pick:直接保留提交;
  • reword:保留改动但修改提交信息;
  • edit:应用提交后暂停,以便修改内容;
  • squash:与前一个提交合并,并编辑提交信息;
  • fixup:与前一个提交合并并丢弃当前提交信息;
  • drop:删除该提交。

交互式 rebase 选择提交

如果把某个提交标记为 edit,Git 会应用该提交后暂停。此时可以修改文件,再用 amend 替换暂停中的提交:

交互式 rebase 停在 edit 提交

修改 edit 提交后的状态

# 修改文件后
git add <>
git commit --amend --no-edit
git rebase --continue

rebase 继续应用后续提交时可能需要处理冲突;如果发生冲突,解决后执行 git addgit rebase --continue。整个提交序列重新应用完成后,原错误提交的修改也会反映在新的提交历史中。

交互式 rebase 完成

想取消则执行 git rebase --abort。交互式 rebase 默认只处理指定上游之后的提交,想从根提交开始可以使用:

git rebase -i --root

想直接丢弃某次提交?

reset --hard:回退当前分支并丢弃工作区改动

git reset --hard HEAD^

这条命令在简单线性历史中会把当前分支(如果 HEAD 附着在分支上)和 HEAD 移动到上一个提交,并把暂存区、已跟踪工作区文件重置为目标提交状态。若处于 detached HEAD,则主要是移动 HEAD,不会移动任何分支。未提交的已跟踪改动会丢失;如果未跟踪文件挡住了目标路径,也可能被覆盖,因此使用前应确认 git status,重要内容先提交、复制或 stash。

reset --hard 回退提交

reset 的三种常见模式:

# 只移动 HEAD/当前分支,保留暂存区和工作区
git reset --soft <commit>

# 移动 HEAD,并把暂存区重置为目标提交;保留工作区
git reset --mixed <commit>
# 不写模式时默认为 mixed

# 移动 HEAD,同时重置暂存区和已跟踪工作区
git reset --hard <commit>

--soft 之后,原 HEAD 到新 HEAD 的差异会出现在暂存区,常用于重新整理最近提交。--mixed 会把差异留在工作区,常用于取消提交但保留文件修改。

reset --soft 保留差异到暂存区

git reset 还有路径形式,不移动 HEAD,只改变暂存区:

# 取消暂存某个文件,但保留工作区改动
git reset HEAD -- <path>
# 现代等价写法
git restore --staged <path>

公共分支已经共享的提交不建议用 reset 改写;需要公开撤销时通常使用 git revert

用交互式 rebase 删除历史提交

如果是个人分支上较早的一次提交,可以使用:

git rebase -i <坏提交的父提>

将对应行改为 drop,或删除该行,然后继续处理冲突。删除提交可能导致后续提交无法干净应用,应检查代码和测试结果。

交互式 rebase 删除提交

用 rebase --onto 移除一段提交

rebase --onto 的一般语法是:

git rebase --onto <新基> <旧基> <>

Git 会把 <分支> 中可达、但不包含在 <旧基础> 中的提交,重新应用到 <新基础> 上。

例如,若 branch1 的历史是 A - B - C - D,想移除 C,可以在确认场景简单且没有合并提交时使用:

git rebase --onto C^ C branch1

这会把 C 之后属于 branch1 的提交重放到 C 的父提交上。原文中的:

git rebase --onto HEAD^^ HEAD^ branch1

只有在特定线性历史和当前 HEAD 位置下才有“移除某个提交”的效果,不能脱离提交图把它当成通用命令。执行前建议用 git log --graph --oneline --decorate --all 确认三个参数的实际位置。

rebase --onto 调整提交范围

错误代码已经 push?

有时代码已经推送到远程,才发现某个提交写错了。处理方式取决于该分支是否已经被别人使用。

错误内容只在自己的功能分支

如果远程分支是自己独立维护的,可以在本地修改、amend、交互式 rebase 或 reset,然后使用更安全的强制推送:

git fetch origin
git push --force-with-lease origin feature/login

--force-with-lease 会检查远程分支仍然是自己预期的旧值,避免不小心覆盖别人刚刚推送的提交。它不是绝对安全,执行前仍应检查分支和远程状态。除非明确知道后果,不要使用无保护的:

git push -f origin feature/login

如果改写历史只是为了修复一个小问题,也可以直接创建新提交,避免强制推送。

问题内容已经进入共享分支

共享分支通常不应删除或改写历史,更适合创建反向提交:

git switch main
git pull --ff-only origin main
git revert <错误提>
git push origin main

git revert 会根据指定提交生成一个新的提交,用相反的变更抵消原提交;原提交仍保留在历史中。对于合并提交,不能简单把 HEAD^ 当作目标,通常需要明确主线父提交:

git revert -m 1 <merge-commit>

-m 1 的含义是把第一个父提交视为主线,具体选择取决于该合并在项目中的语义。回退后仍应运行测试并确认是否需要后续修复提交。

reset:不止可以撤销提交

reset 同时涉及三件事:移动 HEAD/当前分支、更新暂存区、更新工作区。使用前先明确希望改变哪一层:

目标推荐命令
查看差异git diffgit diff --staged
取消暂存但保留文件修改git restore --staged <path>
丢弃工作区某个文件的未暂存改动git restore <path>
整理最近本地提交git reset --soft、交互式 rebase
公开撤销共享提交git revert
丢弃本地已跟踪工作区和暂存区改动git reset --hard 或针对路径的 git restore,但必须确认会丢数据

checkout:历史上的多用途命令

checkout 过去同时承担两类职责:切换分支、检出提交,以及用某个版本恢复工作区文件。为了减少误用,Git 2.23 引入了:

  • git switch:切换或创建分支;
  • git restore:恢复工作区或暂存区文件。

切换分支或进入 detached HEAD

# 推荐
git switch main
git switch -c feature/login
git switch --detach <commit>

# 历史写法,仍然可用
git checkout main
git checkout -b feature/login
git checkout <commit>

直接检出提交会进入 detached HEAD。此时可以查看、构建或临时实验;如果要保留之后创建的提交,应立即创建分支:

git switch -c experiment/keep-this-work

checkout 或 switch 进入 detached HEAD

恢复文件

# 用暂存区版本恢复工作区文件,丢弃该文件未暂存改动
git restore <path>

# 取消暂存,保留工作区修改
git restore --staged <path>

# 从指定提交同时恢复暂存区和工作区(谨慎)
git restore --source=<commit> --staged --worktree <path>

历史命令 git checkout -- <path> 仍可能在旧文章和脚本中出现,但它会直接丢弃工作区文件改动,日常使用时应优先使用语义更明确的 git restore

现代 Git 日常安全工作流

下面是一套适合个人功能开发的示例流程:

# 初始化新项目,并显式指定默认分支名称
git init -b main

git add .
git commit -m "初始化项目"

git remote add origin <远程仓库地>
git push -u origin main

# 开始功能开发
git switch -c feature/login
git status --short
git add -p
git commit -m "实现登录校验"

# 开发期间同步默认分支
git fetch origin
git rebase origin/main

# 推送自己的功能分支
git push -u origin feature/login

# 合并后清理
git switch main
git pull --ff-only origin main
git branch -d feature/login
git push origin --delete feature/login

实际项目还应根据需要配置:

  • .gitignore,避免提交构建产物、依赖目录、日志和密钥;
  • 提交签名、作者信息和提交模板;
  • 分支保护、必须评审和 CI 检查;
  • 远程地址使用 SSH 或 HTTPS 凭据管理;
  • 敏感信息扫描。已经提交过的密钥即使随后删除,也可能留在历史中,需要立即轮换并按情况清理历史。

最后

Git 可以先从四个层次理解:

  1. 工作区:正在编辑的文件;
  2. 暂存区:下一次提交的快照;
  3. 提交图和对象库:不可变的历史对象;
  4. 引用:指向提交的分支、标签、远程跟踪分支和 HEAD。

日常最重要的命令关系是:

git add       工作区 → 暂存区
git commit    暂存区 → 本地提交图
git fetch     远程仓库 → 本地远程跟踪引用
git merge     两条历史合并
 git rebase   重新应用提交并改写提交身份
git push      本地引用 → 远程引用
git restore   从索引或提交恢复文件
git revert    用新提交公开撤销旧提交

理解这些边界后,就不容易把 git add 当成提交、把 git pull 当成永远的 merge、把 branch 当成目录副本,也不会轻易在共享分支上使用 reset --hard 或无保护的强制推送。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS