图解 Git 原理与日常实用指南
Category(分类): git Status: 未知
本文尽量保留原文从版本控制、工作区/暂存区/版本库、团队协作、分支、HEAD、引用、merge、rebase、stash、reflog、diff、amend、reset 和 checkout 等方面的内容,并根据当前 Git 官方文档补充和修正命令语义。
文中历史截图仍保留,但命令示例优先采用现代 Git 推荐的
switch、restore和--force-with-lease。不同 Git 版本、配置、托管平台和默认分支名称可能导致输出略有差异。
从了解版本控制系统开始
所谓版本控制,就是在文件修改的历程中保存版本历史,方便查看差异、回退、比较和协作。一个完整的版本控制流程通常包含:
- 记录文件或项目的历史版本;
- 由开发者主动创建提交(commit);
- 在需要时与远程仓库同步并协作。
远程仓库不是版本控制的必要条件:Git 可以完全在本地使用,远程仓库主要用于备份、共享、代码评审和团队协作。
中央式版本控制系统(VCS)
中央式版本控制系统通常把主要版本库放在服务器上。典型工作模型是:
- 主工程师在公司服务器创建中央仓库并提交项目框架;
- 其他人从服务器获取代码;
- 每个人并行开发,开发完成后把改动提交到中央服务器;
- 其他人再从中央服务器同步最新版本。

中央式系统的优点是权限、版本库和协作入口集中,管理方式较直观;缺点是提交、查看历史等操作往往依赖中央服务器,服务器或网络不可用时工作会受到影响,分支和离线提交能力也通常不如分布式系统灵活。
分布式版本控制系统(DVCS)
分布式版本控制系统除了可以有远程仓库外,团队成员的机器上通常还拥有一份本地仓库。每个人可以在本地提交代码、查看历史、切换分支和进行合并,不必每一步都依赖网络。

典型工作模型是:
- 主工程师在本地创建项目并提交初始版本;
- 在服务器上创建远程仓库,并把本地提交推送到远程仓库;
- 其他人克隆远程仓库,得到工作区和本地 Git 仓库;
- 每个人可以把小改动逐步提交到本地,不必每个中间提交都是完整功能;
- 功能完成后,把相关提交推送到远程仓库;
- 其他人通过
fetch、pull或代码托管平台的合并请求同步和审查这些提交。
默认 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 才会更新下一次提交的内容。
一个文件从未跟踪到提交
- 新建
test.txt并查看状态。因为它还没有被纳入当前提交,Git 会把它显示为Untracked files。

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

git status 中通常会显示 Changes to be committed,而不是原文截图中的拼写错误 commited。需要注意:Git 暂存的是文件在这一时刻的快照,不是一个“永远跟随工作区变化”的标记。
stage表示把改动收集起来以待提交,staging area表示暂存区,staged表示已经进入下一次提交候选快照。
- 文件进入暂存区后,可以创建提交:
git commit -m "添加 test.txt"

不使用 -m 时,Git 会打开编辑器填写提交信息。提交完成后,可以使用下面的命令查看历史:
git log
# 更适合日常浏览的简洁图形视图
git log --oneline --graph --decorate --all
- 再次修改已经被跟踪的文件,运行:
git status
它通常会显示 Changes not staged for commit,表示工作区有改动,但暂存区仍然是上一次 git add 的内容。

再次执行 git add 和 git commit,就会产生新的提交:
git add test.txt
git commit -m "更新 test.txt"
git log --oneline

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

git push 不等于把“本地所有 commit 无条件覆盖到远端”。它按照 refspec 更新指定的远程引用,并传输远端缺少的对象;默认情况下,分支更新通常必须是快进更新,Git 会拒绝可能丢失远端提交的非快进推送。
团队工作基本模型
一种简单的团队流程是:
- 同事在自己的本地分支提交代码,并推送到远程仓库;
- 你先用
git fetch获取远程引用和对象; - 通过代码评审、
git merge、git rebase或git pull把变更整合到本地; - 解决冲突、运行检查后再推送。
可以把常见命令区分为:
# 只获取远程更新,不修改当前工作分支
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 或其他分支策略。
常见的功能分支流程是:
- 为新功能或 bug 修复创建独立分支;
- 在分支上提交小而清晰的变更;
- 推送分支并创建 Pull Request/Merge Request;
- 通过评审、测试和 CI 后合并到默认分支;
- 删除不再使用的本地和远程分支。
如今新仓库的默认分支经常叫 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)。

因此,“当前 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 之后把“切换分支”和“恢复文件”拆成了更明确的 switch 与 restore。
如果在功能分支上提交,HEAD 和该功能分支会一起前移;切回 main 后再提交,则会产生分叉:
删除分支:
# 安全删除:通常要求分支已合并到其上游分支或当前分支
git branch -d feature/login
# 强制删除:即使 Git 判断仍有未合并提交也删除引用
git branch -D feature/login
注意:
- 不能删除当前正检出的分支;先切换到其他分支;
- 删除分支通常只是删除一个引用,不会立即删除提交对象;
- 没有任何引用可达的对象会在 reflog 过期并经过对象清理后被回收,不是删除分支后立刻消失;
-d检查的是合并关系和上游配置,不是固定检查是否合并到名为master的分支;- 删除远程分支不会自动删除其他人的本地分支,其他人还需要清理对应的 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 fetch 或 git pull 刷新。
关于原文中的几个说法,需要修正:
git push不保证只能推送“从远程 clone 或 pull 的分支”。本地新分支可以通过git push -u origin 新分支首次推送;git push默认更新当前配置的上游,但没有上游或配置不允许时需要显式指定;- 远程仓库的
HEAD通常是一个符号引用,指向远程默认分支。默认分支可能是main、master或其他名称,不是永远的master; - 推送普通分支不会让远程默认分支自动改指向新分支。若推送的是默认分支,默认分支本身前移后,远程 HEAD 解析到的提交自然也会改变;
git branch -r可以查看本地远程跟踪分支,git remote show origin可以查看远程信息和默认分支提示。git br -r只有在用户自己配置了br别名时才有效。
开启 Git 操作之旅
merge:合并
在当前分支执行:
git switch main
git merge feature/login
Git 会找到当前分支和目标分支的合并基点,比较两个分支相对于合并基点的变化,并把结果整合到当前分支。

如果当前分支和目标分支已经分叉,默认会创建一个包含两个父提交的 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
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

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

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 的命令位置是:通常站在“需要被重放的分支”上执行,例如站在 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。

错误不是最新的提交:交互式 rebase
交互式 rebase 可以重新排序、编辑、拆分、合并或删除一段尚未公开的提交:
# 选取最近两个提交进行编辑
git rebase -i HEAD~2
HEAD~2 表示沿第一父链向前两个提交。HEAD^^ 在简单的线性历史中通常等价于 HEAD~2,但遇到合并提交时,^ 和 ~ 的含义必须按父提交规则理解:HEAD^1 是第一父提交,HEAD^2 是第二父提交,而 HEAD~2 是沿第一父链走两步。
编辑器中常见操作:
pick:直接保留提交;reword:保留改动但修改提交信息;edit:应用提交后暂停,以便修改内容;squash:与前一个提交合并,并编辑提交信息;fixup:与前一个提交合并并丢弃当前提交信息;drop:删除该提交。

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


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

想取消则执行 git rebase --abort。交互式 rebase 默认只处理指定上游之后的提交,想从根提交开始可以使用:
git rebase -i --root
想直接丢弃某次提交?
reset --hard:回退当前分支并丢弃工作区改动
git reset --hard HEAD^
这条命令在简单线性历史中会把当前分支(如果 HEAD 附着在分支上)和 HEAD 移动到上一个提交,并把暂存区、已跟踪工作区文件重置为目标提交状态。若处于 detached HEAD,则主要是移动 HEAD,不会移动任何分支。未提交的已跟踪改动会丢失;如果未跟踪文件挡住了目标路径,也可能被覆盖,因此使用前应确认 git status,重要内容先提交、复制或 stash。

reset 的三种常见模式:
# 只移动 HEAD/当前分支,保留暂存区和工作区
git reset --soft <commit>
# 移动 HEAD,并把暂存区重置为目标提交;保留工作区
git reset --mixed <commit>
# 不写模式时默认为 mixed
# 移动 HEAD,同时重置暂存区和已跟踪工作区
git reset --hard <commit>
--soft 之后,原 HEAD 到新 HEAD 的差异会出现在暂存区,常用于重新整理最近提交。--mixed 会把差异留在工作区,常用于取消提交但保留文件修改。

git reset 还有路径形式,不移动 HEAD,只改变暂存区:
# 取消暂存某个文件,但保留工作区改动
git reset HEAD -- <path>
# 现代等价写法
git restore --staged <path>
公共分支已经共享的提交不建议用 reset 改写;需要公开撤销时通常使用 git revert。
用交互式 rebase 删除历史提交
如果是个人分支上较早的一次提交,可以使用:
git rebase -i <坏提交的父提交>
将对应行改为 drop,或删除该行,然后继续处理冲突。删除提交可能导致后续提交无法干净应用,应检查代码和测试结果。

用 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 确认三个参数的实际位置。

错误代码已经 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 diff、git 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

恢复文件
# 用暂存区版本恢复工作区文件,丢弃该文件未暂存改动
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 可以先从四个层次理解:
- 工作区:正在编辑的文件;
- 暂存区:下一次提交的快照;
- 提交图和对象库:不可变的历史对象;
- 引用:指向提交的分支、标签、远程跟踪分支和 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 或无保护的强制推送。