这是一篇写给 Git 初学者的图解复习笔记。重点不是背命令,而是先弄清楚"文件现在在哪个区域""分支指针现在指向哪里",再决定应该执行什么命令。
第一次接触 Git 时,我经常遇到三种困惑:
- 明明保存了文件,为什么 Git 还说没有可提交的内容?
add、commit和reset到底在移动什么?- 分支合并后,自己的代码为什么"不见了"或者出现一堆奇怪符号?
这些问题看似零散,实际上都能放进同一张知识地图里。
知识框架

整篇内容分成三条主线:先理解工作区、暂存区和版本库之间的数据流,再学习撤销与回退,最后进入分支、合并、冲突和开发策略。读到任意一节迷路时,都可以回到这张图重新定位。
目录
- 一、为什么需要 Git
- 二、安装 Git、创建仓库与身份配置
- 三、先理解 Git 最重要的三个区域
- 四、提交对象与提交 ID 到底是什么
- 五、用 status、diff、log 和 reflog 看清现场
- 六、版本回退:reset 的三种模式
- 七、撤销修改:先判断内容位于哪个区域
- 八、删除文件:系统删除与 Git 删除不是一回事
- 九、分支、HEAD 与提交:先把指针关系画清楚
- 十、创建、开发、合并与删除:一条完整分支流程
- 十一、合并冲突:Git 为什么不替我做决定
- 十二、Fast-forward 与 --no-ff:结果相似,历史形状不同
- 十三、稳定主分支的开发策略
- 十四、开发到一半出现紧急 bug:使用 stash 暂存现场
- 十五、删除未合并分支:-d 与 -D 不只是大小写区别
- 十六、常见错误与排查顺序
- 十七、一套适合初学者的安全操作习惯
- 十八、总结
一、为什么需要 Git
1. 手动复制文件为什么不够
没有版本管理工具时,我们很容易把项目保存成这样:
project
├── project_最终版
├── project_最终版2
├── project_真的最终版
└── project_真的最终版_不改了
这种方法短期内能用,项目稍微变大就会出现问题:
- 不知道每个版本改了什么;
- 不知道哪个版本可以正常运行;
- 多人修改时很难合并;
- 想撤销某一次修改,只能靠记忆和手工对比;
- 文件越复制越多,却仍然不敢删除。
Git 的价值,就是把"版本、差异、恢复和协作"交给一个可靠的版本控制系统。

可以先把 Git 理解成一个会记账的时间机器:
- 我修改文件;
- 我挑选这一次想保存的内容;
- Git 把这些内容保存为一个提交;
- 以后可以查看、比较或回到某个提交。
2. 文本文件与二进制文件的差别
Git 最擅长管理文本文件,例如:
- C/C++ 源代码;
- Markdown 文档;
- 配置文件;
- HTML、CSS、JavaScript 文件。
对于文本文件,Git 能按行显示"删除了什么、增加了什么"。图片、音频、压缩包等二进制文件也可以交给 Git 保存,但 Git 通常只能判断"文件变了",不容易展示内部每一处变化。

因此,Git 不是不能管理二进制文件,而是对文本差异的展示更直观、更有价值。
二、安装 Git、创建仓库与身份配置
1. 在 Linux 中安装 Git
不同 Linux 发行版使用的包管理命令不同。
bash
# CentOS / RHEL 系列
sudo yum install git
# Ubuntu / Debian 系列
sudo apt install git
# 查看是否安装成功,并显示版本号
git --version
2. 初始化本地仓库
先进入准备交给 Git 管理的目录,再执行:
bash
# 把当前目录初始化为 Git 仓库
git init
执行成功后,当前目录会多出一个隐藏目录 .git。它不是普通项目文件,而是 Git 保存提交对象、分支引用、配置等内部数据的地方。
bash
# Linux 下显示包含隐藏目录在内的全部内容
ls -la
不要随意修改或删除
.git。删除它不会删除工作目录中的源文件,但会让当前目录失去原有的本地版本历史。
3. 配置提交者姓名和邮箱
Git 会在每次提交中记录作者信息。
bash
# 只对当前仓库生效
git config user.name "Your Name"
git config user.email "you@example.com"
# 对当前用户的所有仓库生效
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
查看配置:
bash
# 查看所有能够读取到的配置
git config --list
# 单独查看某个配置项
git config user.name
git config user.email
删除配置时要注意作用域:
bash
# 删除当前仓库中的姓名配置
git config --unset user.name
# 删除全局姓名配置
git config --global --unset user.name
如果本地配置和全局配置同时存在,本地仓库配置的优先级更高。
三、先理解 Git 最重要的三个区域
很多命令记不住,是因为只看到了命令,没有看到数据在什么位置。Git 的本地操作可以先分成三个主要区域:
- 工作区(Working Tree):我正在直接编辑的文件;
- 暂存区(Index / Staging Area):我挑选出来、准备放进下一次提交的内容;
- 版本库(Repository):已经提交、可以长期追踪的历史。
版本库内部还有对象库。文件内容、目录结构和提交说明最终会以对象的形式保存进去。

最常见的数据流只有两步:
工作区 --git add--> 暂存区 --git commit--> 本地版本库
反过来思考也很重要:
- 从工作区撤销修改:让工作区重新采用暂存区中的内容;
- 取消暂存:让暂存区重新采用某个提交中的内容;
- 回退版本:移动当前分支指针,并按模式决定是否同步暂存区和工作区。
1. 完成第一次提交
假设创建了一个 ReadMe 文件:
bash
# 查看当前状态:先确认 Git 发现了哪些变化
git status
# 把 ReadMe 当前内容放入暂存区
git add ReadMe
# 再看一次状态:此时应看到它处于"待提交"状态
git status
# 把暂存区快照保存成一个提交
git commit -m "add ReadMe"
这里最容易产生的误解是:git add 并不是简单地给文件贴一个"已选择"标签。它会把执行命令时的文件内容放入暂存区。
例如:
- 第一次修改
ReadMe; - 执行
git add ReadMe; - 又修改一次
ReadMe; - 直接执行
git commit。
这次提交只会包含第 2 步时进入暂存区的内容;第 3 步的新修改仍留在工作区。
2. 一次添加多个文件
bash
# 只添加指定文件,范围最明确
git add main.c util.c
# 添加当前目录及子目录中的变化
git add .
# 提交暂存区中的全部内容
git commit -m "implement basic functions"
对初学者而言,提交前固定执行一次 git status 很有帮助。它相当于提交前的清单核对。
四、提交对象与提交 ID 到底是什么
每次提交都会得到一个很长的十六进制 ID,例如:
4f7c1f2e0a...
它常被称为提交哈希或提交 ID。实际操作时不一定要输入完整 ID,只要缩写在当前仓库中能够唯一确定目标即可。
一次提交可以先这样理解:
commit 对象
├── 指向一次目录快照(tree)
├── 指向父提交(第一次提交没有父提交)
├── 作者与提交者信息
└── 提交说明
tree 对象
├── 文件名与目录结构
└── 指向文件内容对象(blob)
也就是说,提交对象并不是把每个文件随意堆在一起,而是把"目录结构、文件内容、提交关系和说明"组织成可追踪的历史。
查看提交历史:
bash
# 显示较完整的提交记录
git log
# 每个提交压缩成一行,适合快速查看
git log --oneline
# 同时显示分支关系,后面学习分支时非常有用
git log --oneline --graph --decorate --all
查看对象类型和内容:
bash
# 判断某个对象是 commit、tree 还是 blob
git cat-file -t <对象ID>
# 以可读形式显示对象内容
git cat-file -p <对象ID>
blob保存的是文件内容,不直接保存原始文件名;文件名和目录关系由tree对象组织。
五、用 status、diff、log 和 reflog 看清现场
动手修复问题之前,先看清现场通常比立刻执行命令更安全。
1. git status:现在有哪些变化
bash
# 查看未跟踪、已修改、已暂存等状态
git status
# 使用更精简的两列状态格式
git status --short
status 回答的是:"现在有哪些文件处于什么状态?"
2. git diff:具体改了什么
bash
# 比较"工作区"和"暂存区"
# 适合检查还没有 add 的修改
git diff
# 比较"暂存区"和"HEAD 所指提交"
# 适合检查下一次 commit 准备提交什么
git diff --cached
# 比较两个提交之间的变化
git diff <旧提交ID> <新提交ID>
可以用一句话记忆:
git diff 看还没暂存的内容
git diff --cached 看已经暂存、还没提交的内容
3. git log 与 git reflog 的区别
bash
# 查看正式提交历史
git log --oneline
# 查看 HEAD 和分支引用近期移动过的位置
git reflog
log关注提交历史;reflog关注本地引用如何移动;- 执行错误回退后,
reflog常能帮助找回先前的提交 ID。
reflog 是本地恢复线索,不应被当作永久备份。
六、版本回退:reset 的三种模式
git reset 最核心的动作是移动当前分支指针。三个常用模式的差别,是移动指针后还会不会继续更新暂存区和工作区。

1. --soft:只移动分支指针
bash
# 回到上一个提交,但把撤回的内容保留在暂存区
git reset --soft HEAD^
结果:
- 分支指针回到上一个提交;
- 暂存区保留原提交内容;
- 工作区也保留原内容。
适合:提交说明写错了,或者想把最近几次提交重新整理。
2. --mixed:再重置暂存区
bash
# --mixed 是默认模式
git reset --mixed HEAD^
# 等价的简写
git reset HEAD^
结果:
- 分支指针回退;
- 暂存区恢复到目标提交;
- 工作区文件仍保留修改。
适合:想撤销提交,同时重新选择哪些内容应该 add。
3. --hard:连工作区一起重置
bash
# 高风险:工作区和暂存区都会变成目标提交的状态
git reset --hard HEAD^
结果:
- 分支指针回退;
- 暂存区恢复;
- 工作区也恢复;
- 未提交修改可能直接丢失。
执行前至少检查:
bash
# 第一步:查看是否有未提交修改
git status
# 第二步:确认要回到哪个提交
git log --oneline
# 第三步:确认无误后再使用 --hard
git reset --hard <目标提交ID>
4. HEAD、HEAD^ 与 HEAD~n
HEAD 当前所在位置
HEAD^ 当前提交的第一个父提交
HEAD~2 沿第一个父提交方向向前走两步
在没有合并提交的直线历史中,HEAD^^ 和 HEAD~2 通常指向同一位置。出现合并提交后,^ 还可以用于选择不同父提交,因此不能只把它机械理解成"减一"。
5. 回退错了怎么找回来
bash
# 找到 reset 前 HEAD 曾经指向的提交
git reflog
# 假设找到了目标 ID,再移动回去
git reset --hard <找回的提交ID>
前提是相关提交仍能通过本地对象和引用日志找到。越早恢复,成功机会通常越高。
七、撤销修改:先判断内容位于哪个区域
"我想撤销"并不是一个完整问题。真正需要问的是:
这份错误内容只在工作区,已经进入暂存区,还是已经提交?

场景一:只修改了工作区,还没有 add
bash
# 放弃 ReadMe 在工作区中的修改
# 恢复来源是暂存区中的版本
git checkout -- ReadMe
这里的 -- 用于把命令选项与文件名分开。如果文件名恰好和分支名相同,它也能避免歧义。
执行前建议先看差异:
bash
# 先确认准备放弃哪些内容
git diff ReadMe
# 确认后再撤销
git checkout -- ReadMe
场景二:已经 add,但还没有 commit
要分两步处理。
bash
# 第一步:把 ReadMe 从暂存区撤回工作区
# 文件内容不会因此消失
git reset HEAD ReadMe
# 第二步:如果连工作区修改也不要,再执行
git checkout -- ReadMe
第一步只是"取消暂存",第二步才是"放弃工作区修改"。
场景三:已经 commit
如果提交还只存在于本地,并且确认可以改写这段历史,可以使用:
bash
# 回到上一个提交,同时把撤回内容留在工作区
git reset HEAD^
如果只想完全丢弃最近提交及未提交变化:
bash
# 高风险:请先确认这些内容确实不再需要
git reset --hard HEAD^
一个稳妥的选择顺序是:
- 能用
--soft就先保留暂存内容; - 需要重新挑选内容时用默认的
--mixed; - 只有明确要丢弃工作区变化时才用
--hard。
八、删除文件:系统删除与 Git 删除不是一回事
假设 test.c 已经被 Git 跟踪。
1. 先用系统命令删除
bash
# 只删除工作区文件
rm test.c
# Git 会发现"工作区少了一个文件"
git status
此时删除操作还没有进入暂存区。
如果删错了:
bash
# 从暂存区中的版本恢复工作区文件
git checkout -- test.c
如果确认要删除:
bash
# 把"删除 test.c"这个变化放入暂存区
git add test.c
# 保存删除记录
git commit -m "remove test.c"
2. 使用 git rm
bash
# 同时删除工作区文件,并把删除操作加入暂存区
git rm test.c
# 提交删除记录
git commit -m "remove test.c"
git rm 相当于"删除文件 + 暂存删除变化"。它不会省略最后的 commit。
九、分支、HEAD 与提交:先把指针关系画清楚
分支并不是复制出一整套项目文件。它本质上是一个轻量的引用,指向某个提交。
HEAD 通常指向当前分支,当前分支再指向当前提交:
HEAD → master → 提交 C

本文统一用 master 表示主分支。如果自己的仓库显示的是 main,只需要把示例命令中的 master 换成 main,指针模型和操作逻辑完全相同。
有新提交时,当前分支指针会向前移动:
提交前:HEAD → master → C
提交后:HEAD → master → D
C → D
切换分支,本质上是改变 HEAD 所关联的分支,并让工作区匹配目标分支对应的内容。
1. 查看分支
bash
# 查看本地分支
git branch
输出中带 * 的分支就是当前分支。
2. 创建和切换分支
bash
# 只创建 dev,不切换
git branch dev
# 切换到 dev
git checkout dev
# 创建 dev2 并立即切换过去
git checkout -b dev2
git checkout -b dev2 可以理解为:
bash
git branch dev2
git checkout dev2
3. 在分支上提交
bash
# 确认当前位于哪个分支
git branch
# 修改文件后,把变化放入暂存区
git add ReadMe
# 提交后只有当前分支指针向前移动
git commit -m "update ReadMe on dev"
其他分支不会自动跟着移动,这正是分支可以隔离开发工作的原因。
十、创建、开发、合并与删除:一条完整分支流程

假设要在 dev 中开发功能,完成后合入 master:
bash
# 1. 从当前位置创建并切换到 dev
git checkout -b dev
# 2. 在 dev 中完成修改
git add .
git commit -m "finish feature"
# 3. 切回接收合并结果的 master
git checkout master
# 4. 把 dev 的历史合入当前 master
git merge dev
# 5. 确认不再需要 dev 后,安全删除分支
git branch -d dev
要特别注意合并方向:
先切到"接收结果"的分支,再 merge"提供结果"的分支
因此:
bash
git checkout master
git merge dev
含义是"把 dev 合入 master",不是反过来。
提交、切换、合并前都可以用下面的命令检查:
bash
# 检查工作区是否干净
git status
# 检查当前分支
git branch
# 检查历史形状和各分支位置
git log --oneline --graph --decorate --all
十一、合并冲突:Git 为什么不替我做决定
如果两个分支修改了不同文件,或者修改了同一文件的不同位置,Git 通常可以自动合并。
如果两个分支修改了同一个文件的同一部分,Git 无法判断哪一边才是正确业务结果,于是停止合并并标记冲突。

冲突文件中常见的标记如下:
<<<<<<< HEAD
当前分支中的内容
=======
被合入分支中的内容
>>>>>>> dev
这些符号的含义:
<<<<<<< HEAD到=======:当前分支的内容;=======到>>>>>>> dev:准备合入的dev内容;- Git 只负责指出两边差异,不会替我理解业务需求。
正确解决步骤
bash
# 1. 查看哪些文件发生冲突
git status
# 2. 打开冲突文件,人工决定最终内容
# 删除 <<<<<<<、=======、>>>>>>> 这些冲突标记
# 3. 把解决后的文件加入暂存区
git add ReadMe
# 4. 创建合并提交,结束这次合并
git commit -m "resolve merge conflict"
只编辑文件还不够。必须再次 git add,再 git commit,Git 才知道冲突已经解决并完成合并。
如果暂时不想继续本次合并,可以使用:
bash
# 放弃当前未完成的合并,尽量回到合并开始前
git merge --abort
十二、Fast-forward 与 --no-ff:结果相似,历史形状不同

1. Fast-forward:快进合并
如果 master 指向的提交正好是 dev 的祖先,那么:
bash
git checkout master
git merge dev
Git 可以直接把 master 指针移动到 dev 所在位置,不需要创建新的合并提交。
优点:历史简洁。
不足:从提交图上不容易看出某一段开发曾经属于独立分支。
2. --no-ff:明确创建合并提交
bash
# 即使可以快进,也创建一个合并提交
git merge --no-ff -m "merge dev" dev
这样会保留一个明确的合并节点,历史中更容易看出功能分支的边界。
两种模式不一定会产生不同的最终文件内容,但会产生不同的提交历史形状。
十三、稳定主分支的开发策略
当所有人都直接在主分支上修改,任何一个未完成功能都可能影响稳定版本。更清晰的做法是分层管理:
master:目标是保持稳定、可发布;dev:用于日常开发和集成;feature/*:每个功能单独开发,完成并验证后再合入dev。

一个功能分支可以这样完成:
bash
# 1. 先进入开发分支
git checkout dev
# 2. 从 dev 创建独立功能分支
git checkout -b feature/login
# 3. 完成功能并提交
git add .
git commit -m "implement login"
# 4. 回到 dev,接收功能结果
git checkout dev
git merge feature/login
# 5. 功能分支已合入,可以安全删除
git branch -d feature/login
当 dev 中多个功能完成整体验证后,再把它合入 master:
bash
git checkout master
git merge dev
更稳妥的集成顺序
假设 master 在开发期间增加了一个紧急修复,而 dev 也有自己的开发提交。直接把 dev 合入 master,可能到最后一步才暴露冲突。
更稳妥的流程是:
- 先在
dev中合入最新master; - 在
dev中解决冲突并完成验证; - 再把验证后的
dev合入master。

对应命令:
bash
# 第一步:让 dev 先吸收 master 的最新变化
git checkout dev
git merge master
# 第二步:如有冲突,在 dev 中解决、测试并提交
git status
# 第三步:确认 dev 已经可用,再合回 master
git checkout master
git merge dev
这样可以把冲突处理和验证工作留在开发分支,减少主分支长时间处于不完整状态的机会。
十四、开发到一半出现紧急 bug:使用 stash 暂存现场
假设我正在 dev2 开发,修改还不适合提交,但此时必须立刻回 master 修复紧急问题。
直接切换分支可能被 Git 拒绝,也可能把未提交变化带到另一个分支。可以先使用 stash 临时收起工作现场。

完整流程如下:
bash
# 1. 确认当前状态和分支
git status
git branch
# 2. 临时保存已跟踪文件的工作区修改和暂存内容
git stash
# 3. 回到 master
git checkout master
# 4. 创建紧急修复分支
git checkout -b fix_bug
# 5. 修复问题并提交
git add .
git commit -m "fix bug"
# 6. 把修复结果合回 master
git checkout master
git merge fix_bug
# 7. 回到原来的开发分支
git checkout dev2
# 8. 恢复工作现场,并移除对应 stash 记录
git stash pop
stash 的几个常用查看命令
bash
# 查看保存过的工作现场
git stash list
# 恢复最近一次现场,但保留 stash 记录
git stash apply
# 恢复最近一次现场,并在成功后移除记录
git stash pop
默认情况下,未跟踪文件不会被 git stash 收起。如果确实需要一起保存:
bash
# -u 表示同时包含未跟踪文件
git stash -u
stash 适合临时切换任务,不应长期代替清晰的提交。
十五、删除未合并分支:-d 与 -D 不只是大小写区别

1. 优先使用 -d
bash
# 安全删除:如果分支存在未合并提交,Git 通常会拒绝
git branch -d dev
-d 会帮我做一次保护性检查。若分支内容已合并,删除的主要是分支引用,提交仍可从合入后的历史访问。
2. 强制删除前先看独有提交
bash
# 查看 dev 中存在、master 中不存在的提交
git log --oneline master..dev
如果这里仍有输出,说明 dev 还有独有提交。只有明确决定放弃它们时,才考虑:
bash
# 高风险:即使尚未合并,也强制删除 dev 分支引用
git branch -D dev
删除分支引用后,独有提交不一定立刻从对象库消失,但寻找它们的直接入口可能丢失。不要把"暂时可能找回"当成安全保障。
最容易记住的原则是:
能用 -d 就不用 -D;
只有明确放弃独有提交时,才强制删除。
十六、常见错误与排查顺序
1. git commit 后没有包含刚改的内容
可能原因:修改发生在最后一次 git add 之后。
排查:
bash
# 查看哪些内容还留在工作区
git diff
# 查看暂存区准备提交什么
git diff --cached
2. 合并方向弄反
错误思路:看到 git merge dev,就以为当前会切到 dev。
正确理解:merge 不会替我切换分支,它把指定分支合入当前分支。
bash
# 先确认当前分支
git branch
# 要把 dev 合入 master,必须先切到 master
git checkout master
git merge dev
3. 冲突文件已经修改,但 Git 仍说正在合并
原因:只删除了冲突标记,没有重新暂存并提交。
bash
git add <已解决的文件>
git commit -m "resolve merge conflict"
4. reset --hard 后修改不见了
--hard 本来就会同步重置工作区。可以尽快查看:
bash
# 寻找 reset 前的提交位置
git reflog
未提交的工作区修改通常没有对应提交,恢复难度远高于已经提交的内容。
5. 切换分支时 Git 拒绝操作
常见原因:当前未提交修改会被目标分支内容覆盖。
可以根据实际情况选择:
bash
# 方案一:修改已经完整,正常提交
git add .
git commit -m "save current work"
# 方案二:修改还不适合提交,临时收起
git stash
不要为了强行切换而随手使用会丢失内容的命令。
十七、一套适合初学者的安全操作习惯
执行普通操作前:
bash
# 我在哪个分支?
git branch
# 当前有哪些变化?
git status
# 具体改了什么?
git diff
git diff --cached
执行回退或删除前:
bash
# 提交历史是什么样?
git log --oneline --graph --decorate --all
# 目标分支是否有独有提交?
git log --oneline master..dev
# HEAD 最近移动过哪些位置?
git reflog
可以把安全优先级记成四句话:
- 先
status,再操作; - 先看
diff,再放弃修改; - 先看分支图,再回退;
- 能保留内容,就不要急着使用
--hard或-D。
十八、总结
Git 本地版本管理的核心,不是命令数量,而是三组关系:
- 三个区域:工作区、暂存区、版本库;
- 三个指针层次 :
HEAD → 当前分支 → 当前提交; - 三类安全判断:内容在哪里、分支指向哪里、操作会覆盖什么。
最后用一组最小命令表完成复习:
bash
# 初始化与配置
git init
git config user.name "Your Name"
git config user.email "you@example.com"
# 保存版本
git status
git add .
git commit -m "message"
# 查看变化与历史
git diff
git diff --cached
git log --oneline --graph --decorate --all
git reflog
# 撤销与回退
git checkout -- <文件>
git reset HEAD <文件>
git reset --soft HEAD^
git reset HEAD^
git reset --hard HEAD^
# 分支
git branch
git checkout -b dev
git checkout master
git merge dev
git branch -d dev
# 临时保存现场
git stash
git stash list
git stash pop
当我能先在脑中回答"这条命令会移动哪个指针、更新哪个区域",Git 就不再是一堆容易混淆的咒语,而会变成一套可以推理、可以验证的工具。