Git 学习笔记
一份完整的 Git 学习笔记。重点讲"为什么这么设计"(费曼式 📌 讲解),配实操示例。以本项目 RyBOS 的真实 Git 历史为案例,学完就能上手。
怎么用这份笔记
- 学习/复习 → 顺序看正文,重点看 📌 类比和"为什么"
- 查命令 → 翻 附录 A 速查手册
- 排错 → 翻 附录 B 常见问题速查
- 落地 → 跟 附录 C 实战场景做一遍
图表在 /Git学习笔记/diagrams/ 目录。图注名即原图文件名,看图注就能直接找到原图用 draw.io 编辑。
1 · Git 概述与架构
1.1 Git 是什么
- Git 是一个分布式版本控制系统(DVCS),由 Linus Torvalds 于 2005 年创建
- 最初是为了管理 Linux 内核源码而设计(在此之前 Linux 内核用的是 BitKeeper,因授权争议被收回)
- 设计目标只有一个:快、适合超大规模项目、完全分布式
TIP
📌 打个比方:Git 像一台"时光机 + 平行宇宙管理器"。时光机让你随时回到项目的任何一个历史时刻;平行宇宙管理器让你在不影响主线的情况下开辟多条实验路线(分支),满意的合并回来,不满意的直接丢弃。
1.2 集中式 vs 分布式
|
集中式(SVN) |
分布式(Git) |
| 仓库形态 |
一个中央服务器,客户端只存工作副本 |
每个开发者都有完整仓库(含全部历史) |
| 离线工作 |
否(提交/查看历史必须联网) |
是(commit/diff/log 全部本地完成) |
| 单点故障 |
中央服务器挂了就全完 |
任何一份克隆都是完整备份 |
| 分支代价 |
创建分支是复制整个目录,很重 |
分支只是一个指针(41 字节),极轻 |
| 速度 |
每次操作都要网络往返 |
本地操作,毫秒级 |
TIP
📌 为什么 Git 的分支这么轻?因为 Git 的分支不是一个"目录副本",而只是一个指向某个 commit 的指针。创建分支就是写一个 41 字节的文件(40 字节 SHA-1 + 换行符),几乎零成本。在 SVN 时代,分支是"大事",要开会讨论;在 Git 时代,分支是"日常",随手创建、随手丢弃。

1.3 Git 整体架构
┌──────────────────────────────────────────────┐
│ 远程仓库 │
│ (GitHub/Gitee/...) │
│ origin: https://gitee.com/... │
└──────────────┬───────────────────────────────┘
│ fetch/pull ←──── push
│
┌──────────────┴───────────────────────────────┐
│ 本地仓库 (.git/) │
│ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ 工作区 │→│ 暂存区 │→│ 版本库 │ │
│ │ Working │ │ Staging │ │ Repository │ │
│ │ Directory│ │ Area │ │ (.git/) │ │
│ └─────────┘ └──────────┘ └──────────────┘ │
│ ↑ checkout ↑ add ↑ commit │
│ │ │ │ │
│ 你编辑文件 git add git commit │
└─────────────────────────────────────────────────┘
1.4 .git 目录里有什么
.git/
├── HEAD # 指向当前分支:ref: refs/heads/master
├── config # 仓库配置(remote URL、用户信息等)
├── hooks/ # 钩子脚本(pre-commit、commit-msg 等)
├── info/ # 全局排除规则(exclude 文件)
├── objects/ # 所有 Git 对象(blob、tree、commit、tag),按 SHA-1 存储
├── refs/ # 引用(分支和标签的指针)
│ ├── heads/ # 本地分支:master、dev 等
│ └── remotes/ # 远程跟踪分支:origin/master 等
└── logs/ # reflog:HEAD 和分支的操作日志
TIP
📌 理解 .git 就是理解 Git。objects/ 存所有版本快照,refs/ 存所有指针,HEAD 存"你现在在哪"。这三样组合起来就是 Git 的全部核心。
1.5 Git 对象模型

Git 存储的每种数据都是不可变对象,用 40 字符 SHA-1 哈希唯一标识:
| 对象类型 |
存什么 |
类比 |
| blob |
文件内容(不含文件名) |
照片里的"画面" |
| tree |
目录快照(文件名 → blob 的映射) |
照片里的"构图" |
| commit |
一次提交(指向 tree + parent + 元信息) |
一张"快照照片" |
| tag |
附注标签(指向 commit + 标签信息) |
照片上贴的"标签贴纸" |
TIP
📌 为什么用哈希寻址?内容相同的文件哈希相同,Git 天然去重——改一个文件只产生一个新 blob,其他文件的 blob 复用旧对象,省空间。而且哈希是内容校验,任何人篡改历史都会导致哈希不匹配,立刻被发现。
本项目 .git/config 实例:
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
ignorecase = true
[remote "origin"]
url = https://gitee.com/hisos/ry-bos.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
2 · 核心概念:三个区域
2.1 工作区、暂存区、版本库
工作区 (Working Directory) 暂存区 (Staging Area) 版本库 (Repository)
你能看到的文件 git add 后的快照 git commit 后的永久记录
────────────── ────────────── ──────────────
随意修改 临时存放区 不可变的历史快照
│ │ │
│──── git add ──────────────→│ │
│ │──── git commit ──────────→│
│←── git checkout/restore ───│←── git reset ─────────────│
TIP
📌 打个比方:工作区是你的"书桌",文件摊在上面随便改;暂存区是"购物车",把想买的东西先放进去,还能调整;版本库是"已下单的订单",一旦提交就成了历史记录,不可修改(只能新提交来覆盖)。
为什么要有暂存区?你可能改了 5 个文件,但只想提交其中 3 个。暂存区让你精确控制每次提交的内容。这是 Git 区别于其他 VCS 的关键设计。
2.2 文件的四种状态
| 状态 |
含义 |
命令 |
| Untracked |
新文件,Git 还没跟踪 |
git add → Staged |
| Staged |
已加入暂存区,等待提交 |
git commit → Committed |
| Committed |
已提交到版本库 |
编辑文件 → Modified |
| Modified |
已跟踪文件被修改,还没暂存 |
git add → Staged |
2.3 用 status 看清当前状态
$ git status
On branch master # 当前在 master 分支
Your branch is up to date with 'origin/master'.
Changes to be committed: # 暂存区(即将进入 commit)
new file: qt/NewModule/src/NewModule.h
Changes not staged for commit: # 工作区已修改但未暂存
modified: cpp/CRC/src/CRC.cpp
Untracked files: # 新文件,Git 还没跟踪
qt/NewModule/example/main.cpp
TIP
📌 养成 git status 习惯:每次操作前后看一眼,就像开车看仪表盘。新手 90% 的 Git 问题是"不知道自己当前在什么状态"——以为提交了其实没提交,以为在 dev 分支其实在 master。
3 · 基础操作
3.1 创建仓库
# 从零创建
mkdir my-project && cd my-project
git init
# 克隆已有仓库
git clone https://gitee.com/hisos/ry-bos.git
# 只克隆最近一次提交(大仓库省时间)
git clone --depth 1 https://gitee.com/hisos/ry-bos.git
# 克隆指定分支
git clone -b master https://gitee.com/hisos/ry-bos.git
TIP
📌 git init --bare:创建裸仓库(没有工作区,只存 .git/ 内容),用于做远程中央仓库。远程服务器上的中央仓库就是裸仓库——它只负责存储和转发,不需要人直接在里面改文件。
3.2 添加与提交
# 添加单个文件
git add README.md
# 添加所有修改和新文件
git add .
# 交互式选择要添加的改动(同一文件的部分修改)
git add -p # p = patch,逐块确认
# 提交
git commit -m "feat: 新增 AsyncTask 异步任务模块"
# 带详细描述
git commit -m "fix: 修复 CRC 计算溢出" -m "uint16_t 溢出改用 uint32_t"
# 跳过暂存区,直接提交所有已跟踪文件的修改(不包括新文件)
git commit -am "docs: 更新 README"
TIP
📌 git add -p 是杀手级功能:同一文件改了 5 处,但只有 3 处属于"功能 A"。用 git add -p 逐块选择,把 3 处先提交为一个 commit,再提交剩下 2 处。让每个 commit 只做一件事。
📌 -am 不包括新文件:-a 只自动暂存已跟踪文件的修改,对 Untracked 文件无效。新文件必须先 git add。
提交信息规范(Conventional Commits)
本项目 RyBOS 真实提交示例:
| 前缀 |
含义 |
示例 |
feat: |
新功能 |
feat: 新增 ToolBox 工具箱 - 3DTiles双引擎查看器 |
fix: |
修复 bug |
fix: --force用PDF_NODES记录的nodeId直接删旧文件 |
docs: |
文档变更 |
docs: sync live template swimlane rules |
refactor: |
重构 |
refactor: 编译组织优化 |
chore: |
构建/工具 |
chore: 更新依赖版本 |
3.3 查看差异
# 工作区 vs 暂存区(还没 add 的改动)
git diff
# 暂存区 vs 版本库(已 add 还没 commit 的改动)
git diff --cached
# 工作区 vs 版本库(所有未提交的改动)
git diff HEAD
# 比较两个分支
git diff master..dev
# 只看哪些文件变了
git diff --stat
TIP
📌 git diff 的三种境界:
git diff — "我改了什么还没存"(工作区 → 暂存区)
git diff --cached — "我存了什么还没提交"(暂存区 → 版本库)
git diff HEAD — "相比上次提交,总共变了什么"(工作区 → 版本库)
:::
3.4 删除与移动文件
# 从工作区和暂存区都删除
git rm old-file.txt
# 只从暂存区移除,保留工作区文件(让 Git 不再跟踪)
git rm --cached config.local.txt
# 重命名/移动文件
git mv old_name.txt new_name.txt
:::tip
📌 git rm --cached 的用途:不小心把不该跟踪的文件(如 .env)git add 了,用 git rm --cached 移除跟踪但保留本地文件,然后加入 .gitignore。
4 · 查看历史
4.1 git log — 提交历史
# 每个提交一行(最常用)
git log --oneline
# 带分支图(看合并关系)
git log --oneline --graph --all
# 本项目实例
$ git log --oneline --graph --all -10
* 481ac6e (HEAD -> master, origin/master) knowledge: 添加 Linux 学习笔记
* e5b1ac1 Sync live doc template and role skills
* ec16745 docs: sync live template swimlane rules
* fc836d4 docs: require web server hop in swimlanes
* b72d61a docs: clarify swimlane participant rules
* e8eeec1 docs: tighten swimlane response routing rules
* 4ba995a docs: fix swimlane watermark orientation
* 31f49a3 docs: make swimlane watermarks horizontal
* 739b8b9 docs: add swimlane watermarks to live template
* ebc0b5c docs: add live doc template and skills
常用 log 选项
| 选项 |
含义 |
记忆 |
--oneline |
每个提交压缩成一行 |
简洁模式 |
--graph |
ASCII 分支图 |
可视化合并历史 |
--all |
显示所有分支 |
不只看当前分支 |
-n N |
只看最近 N 条 |
限制数量 |
--author="hisos" |
按作者过滤 |
找某人的提交 |
--since="2026-01-01" |
按日期过滤 |
时间范围 |
--grep="fix" |
按提交信息过滤 |
关键词搜索 |
--stat |
显示文件变更统计 |
看改了哪些文件 |
-p |
显示完整 diff |
看改了什么内容 |
--follow |
追踪文件重命名历史 |
看某个文件的完整演变 |
实用组合
# 找某个文件的修改历史(含重命名追踪)
git log --follow --oneline -- qt/VideoPlayer/src/QtVideoThread.h
# 看谁在什么时间改了某文件
git log --format="%h %an %ad %s" --date=short -- cpp/CRC/src/CRC.cpp
# 搜索提交信息含 "fix" 的提交
git log --oneline --grep="fix"
# 看最近一周的提交
git log --oneline --since="1 week ago"
# 统计每人提交次数
git shortlog -sn
4.2 git show — 查看单个提交
# 查看某个提交的详情(改了什么)
git show 481ac6e
# 只看改了哪些文件
git show 481ac6e --stat
# 查看某个提交对特定文件的修改
git show 8e3db40 -- qt/ToolBox/
4.3 git blame — 逐行追溯
# 看每行是谁、在哪个提交、什么时间写的
git blame README.md
# 只看第 10-20 行
git blame -L 10,20 README.md
# 忽略空格改动
git blame -w README.md
TIP
📌 git blame 的真正用途:不是甩锅,而是追溯某行代码的上下文——这行代码是在哪个 commit 加的?提交信息说了什么原因?看到可疑代码,先 git blame 找到 commit,再 git show 看完整改动和说明。
5 · 分支管理

5.1 分支的本质
分支就是一个指向某个 commit 的可变指针。
commit-1 ← commit-2 ← commit-3 ← commit-4
↑
master(指针)
HEAD → master(指向当前分支)
HEAD 是特殊指针,指向当前所在的分支
- 分支指针随着每次 commit 自动前移
- 创建分支 = 创建一个新指针,不复制任何文件
# 查看当前 HEAD 指向
$ cat .git/HEAD
ref: refs/heads/master
# 查看分支指针指向哪个 commit
$ cat .git/refs/heads/master
481ac6e...(40 字符 SHA-1)
TIP
📌 为什么 Git 创建分支是 O(1) 操作?因为创建分支只是往 .git/refs/heads/ 目录写一个 41 字节的文件。不复制代码、不创建目录、不占空间。对比 SVN 创建分支需要复制整个目录树,Git 的分支轻到几乎不存在——这就是为什么 Git 鼓励频繁创建和丢弃分支。
5.2 分支操作
# 查看所有本地分支
git branch
# 查看所有分支(含远程)
git branch -a
# 本项目实例
$ git branch -a
* master # * 表示当前所在分支
remotes/origin/HEAD -> origin/master
remotes/origin/master
# 创建分支(但不切换过去)
git branch dev
# 创建并切换到新分支
git checkout -b dev # 旧写法
git switch -c dev # 新写法(Git 2.23+,语义更清晰)
# 切换分支
git checkout master # 旧写法
git switch master # 新写法
# 删除分支(安全删除:未合并则拒绝)
git branch -d dev
# 强制删除分支(即使未合并也删)
git branch -D dev
# 重命名分支
git branch -m old-name new-name
TIP
📌 checkout vs switch:git checkout 历史上承担了太多职责(切换分支、恢复文件、创建分支),容易混淆。Git 2.23 引入了 switch(专门切换分支)和 restore(专门恢复文件),让语义更清晰。新项目建议用 switch/restore,但 checkout 仍然兼容。
5.3 分支命名规范
main / master # 主分支(生产环境代码)
develop # 开发分支(集成测试)
feature/xxx # 功能分支(如 feature/video-player)
fix/xxx # 修复分支(如 fix/crc-overflow)
hotfix/xxx # 紧急修复(直接从 main 拉,修完合回 main)
release/x.x.x # 发布分支(准备发版时的预发布分支)
6 · 合并与变基

6.1 git merge — 合并分支
# 把 dev 分支合并到当前分支(master)
git merge dev
Fast-forward 合并
当目标分支是当前分支的直接后继时,Git 只需把指针往前移:
合并前: A ← B ← C (master) ← D ← E (dev)
合并后: A ← B ← C ← D ← E (master) 指针直接前移
三方合并(3-way merge)
当两个分支各有新提交时,Git 创建一个合并提交(有两个父节点):
合并前: A ← B ← C (master)
\
D ← E (dev)
合并后: A ← B ← C ←── M (master,合并提交)
\ /
D ←── E (dev)
TIP
📌 为什么叫"三方合并"?Git 找三个点:当前分支末尾(C)、目标分支末尾(E)、以及两个分支的最近公共祖先(B)。对比 B→C 和 B→E 的差异,自动判断哪些互不冲突、哪些需要人工解决。公共祖先是关键——没有它,Git 无法区分"你改的"和"对方改的"。
禁用 fast-forward(保留合并记录)
git merge --no-ff dev # 强制创建合并提交,保留分支历史
TIP
📌 --no-ff 的价值:fast-forward 合并后,从历史中看不出"这些提交是在 dev 分支上做的"。--no-ff 强制创建合并提交,在历史中留下明确的"合并点",方便回溯。团队协作中推荐 --no-ff。
6.2 git rebase — 变基
# 把当前分支的提交"搬到" master 最新提交之上
git rebase master
rebase 前: A ← B ← C (master) ← D ← E (dev,当前在此)
rebase 后: A ← B ← C ← D' ← E' (dev,D 和 E 被重新创建)
TIP
📌 rebase 的本质是"改写历史"。它把 dev 分支上的提交一个个"摘下来",在 master 最新位置重新"播放"一遍,生成新的 commit(SHA-1 变了)。结果是一条直线。
merge vs rebase 的哲学区别:
- merge:保留完整历史,"这些事在 dev 上发生了,然后在某个时间点合并了"。历史是真实发生过的。
- rebase:美化历史,"这些事是在 master 最新基础上发生的"。历史是重构过的故事。
选择建议:
- 本地分支同步主线最新代码 → 用 rebase(保持线性历史)
- 合并功能分支到主线 → 用 merge(保留合并记录)
:::
rebase 冲突处理
git rebase master
# 冲突时 Git 暂停
# 解决冲突后
git add <冲突文件>
git rebase --continue
# 放弃 rebase
git rebase --abort
6.3 黄金法则
:::warning
🚧 永远不要对已推送到远程、且别人可能基于它工作的分支执行 rebase。因为 rebase 会重写 commit SHA-1,别人的本地分支会和你的产生分叉,导致冲突地狱。
安全规则:rebase 只在本地未推送的提交上用。一旦 push 了,就只用 merge。
7 · 冲突解决
7.1 冲突长什么样
当两个分支修改了同一文件的同一区域,Git 无法自动合并:
<<<<<<< HEAD
这是当前分支(master)的内容
=======
这是被合并分支(dev)的内容
>>>>>>> dev
7.2 解决冲突的流程
git merge dev
# Auto-merging failed; fix conflicts and then commit the result.
# 1. 查看哪些文件冲突
git status
# both modified: cpp/CRC/src/CRC.cpp
# 2. 打开冲突文件,手动编辑
# 删除冲突标记(<<<<<<<、=======、>>>>>>>)
# 保留想要的代码,或合并两边的代码
# 3. 标记冲突已解决
git add cpp/CRC/src/CRC.cpp
# 4. 完成合并
git commit -m "merge: 合并 dev 分支,解决 CRC.cpp 冲突"
7.3 冲突解决工具
# 用 VS Code 作为合并工具
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
# 启动合并工具
git mergetool
TIP
📌 减少冲突的最佳实践:
- 频繁拉取:每天开始工作前
git pull
- 小步提交:改一点提交一点,不要攒一大堆
- 短命分支:功能分支尽快合并,不要拖几周
- 沟通协调:避免两个人同时改同一文件
- 模块化:不同功能在不同文件里,天然减少冲突
:::
8 · 远程仓库
8.1 远程仓库概念
本地仓库 远程仓库 (origin)
┌──────────┐ ┌──────────────┐
│ master │ ──── push ────→ │ origin/master │
│ (本地) │ ←── pull/fetch ─ │ (远程) │
└──────────┘ └──────────────┘
origin:远程仓库的默认名称(clone 时自动设置)
origin/master:远程跟踪分支,记录"远程 master 上次同步时的位置",只读
master:本地分支,你实际工作的地方
:::tip
📌 origin/master 不是远程的实时状态。它是你上次 fetch/pull 时记下来的快照。远程仓库可能已经更新了,但你的 origin/master 不会自动变。必须 git fetch 才能更新它。这就是为什么 git status 说 "up to date with origin/master" 时,只代表你本地和上次同步时一样,不代表远程没有新提交。
8.2 远程操作命令
# 查看远程仓库
git remote -v
# origin https://gitee.com/hisos/ry-bos.git (fetch)
# origin https://gitee.com/hisos/ry-bos.git (push)
# 添加远程仓库
git remote add origin https://gitee.com/hisos/ry-bos.git
# 修改远程仓库地址
git remote set-url origin https://gitee.com/hisos/new-repo.git
# 删除远程仓库
git remote remove origin
8.3 fetch vs pull vs push
# fetch:下载远程更新到 origin/master,但不合并到本地 master
git fetch origin
git fetch --all # 拉取所有远程
# fetch 后查看远程有什么新东西
git log origin/master --oneline -5
git diff master..origin/master # 看差异
# 确认后再合并
git merge origin/master
# pull = fetch + merge(一步到位)
git pull
git pull --rebase # pull = fetch + rebase(更干净的历史)
# push:推送本地提交到远程
git push origin master
git push # 简写(当前分支已设置上游时)
git push -u origin dev # 首次推送 dev 分支并设置上游跟踪
git push --force # 强制推送(危险!覆盖远程历史)
git push --force-with-lease # 安全的强制推送(只在远程没有新提交时才覆盖)
TIP
📌 fetch vs pull:
git fetch:只下载,不合并。先看看远程有什么新东西,决定要不要合并。安全、可控。
git pull:下载 + 自动合并。一步到位,但可能有意外冲突。方便。
建议:不确定时用 fetch + 手动 merge,日常用 pull --rebase。
📌 --force vs --force-with-lease:
--force 无条件覆盖远程,可能冲掉别人的提交。--force-with-lease 会先检查远程分支有没有被别人更新过,如果有则拒绝推送。永远用 --force-with-lease 代替 --force。
8.4 SSH vs HTTPS
# HTTPS(每次可能要输密码,可配置 credential helper 缓存)
git clone https://gitee.com/hisos/ry-bos.git
# SSH(配置好密钥后免密,推荐)
git clone git@gitee.com:hisos/ry-bos.git
TIP
📌 SSH 配置一次,终身免密:
# 1. 生成 SSH 密钥对
ssh-keygen -t ed25519 -C "your_email@example.com"
# 2. 把公钥 (~/.ssh/id_ed25519.pub) 添加到 Gitee/GitHub 的 SSH Keys
# 3. 测试连接
ssh -T git@gitee.com
# 4. 把已有仓库从 HTTPS 切到 SSH
git remote set-url origin git@gitee.com:hisos/ry-bos.git
9 · 标签管理
9.1 为什么用标签
分支是"移动的指针",标签是"固定的锚点"。发布 v1.0 时,给那个 commit 打个标签,以后任何时候都能精确回到那个版本。
9.2 标签操作
# 查看所有标签
git tag
# 创建轻量标签(只是一个指向 commit 的指针)
git tag v1.0
# 创建附注标签(推荐,包含作者、日期、说明信息)
git tag -a v1.0 -m "发布版本 1.0:包含 CRC、AsyncTask、ConfigManager 等 20 个模块"
# 给历史提交打标签
git tag -a v0.9 -m "早期版本" 8e3db40
# 查看标签信息
git show v1.0
# 推送标签到远程(默认 push 不推标签)
git push origin v1.0 # 推单个标签
git push origin --tags # 推所有标签
# 删除标签
git tag -d v1.0 # 删本地
git push origin --delete v1.0 # 删远程
TIP
📌 轻量标签 vs 附注标签:轻量标签只是一个文件存着 commit SHA-1。附注标签是一个完整的 Git 对象,记录了谁打的标签、什么时候打的、为什么打。正式发布永远用附注标签(-a)。
10 · 工作流模型

10.1 Git Flow(经典模型)
master ────●───────────────●──────────────●───── (生产环境)
\ ↑ ↑
\ release hotfix
\ / v1.0 /
develop ──────●──●──●──●──●──●──●──●──●──●────── (开发主线)
\ / \ /
●──●──● ●──●──●
feature/ feature/
| 分支 |
来源 |
合并到 |
生命周期 |
master |
- |
- |
永久 |
develop |
master |
master(发版时) |
永久 |
feature/* |
develop |
develop |
临时 |
release/* |
develop |
develop + master |
临时 |
hotfix/* |
master |
master + develop |
临时 |
适合:有固定发版周期的中大型项目。
10.2 GitHub Flow(简化模型)
master ──●──●──●──●──●──●──●──●── (随时部署)
\ / \ /
●──●──● ●──●──●
feature/ feature/
规则极简:master 永远可部署 → 拉分支开发 → 提 PR → Review 通过后合并 → 立即部署。
适合:持续部署的 Web 项目、小团队。
10.3 本项目的工作流
本项目 RyBOS 采用简化模型:
- 只有
master 一个长期分支
- 直接在 master 上提交(单人项目)
- 提交信息用 Conventional Commits 规范
- 托管在 Gitee(
https://gitee.com/hisos/ry-bos.git)
11 · 撤销与回退

11.1 撤销操作全景图
场景 命令
─────────────────────────── ──────────────────────────────
工作区修改了,还没 add git restore <file>
已 add 到暂存区,想撤回 git restore --staged <file>
已 commit,想修改提交信息 git commit --amend
已 commit,想撤销(保留改动) git reset --soft HEAD~1
已 commit,想撤销(暂存改动) git reset --mixed HEAD~1(默认)
已 commit,想彻底丢弃 git reset --hard HEAD~1(危险!)
已 push,想撤销 git revert <commit>(安全)
11.2 git reset — 移动 HEAD 指针
# --soft:只移动 HEAD,暂存区和工作区不变
# 效果:撤销 commit,改动还在暂存区(可以直接重新 commit)
git reset --soft HEAD~1
# --mixed(默认):移动 HEAD,暂存区重置,工作区不变
# 效果:撤销 commit + 撤销 add,改动还在工作区
git reset HEAD~1
# --hard:移动 HEAD,暂存区和工作区都重置
# 效果:彻底丢弃所有改动,回到指定 commit 的状态
git reset --hard HEAD~1
TIP
📌 reset 三种模式的区别:
HEAD 指针 暂存区 工作区
--soft 移动 不变 不变
--mixed(默认) 移动 重置 不变
--hard 移动 重置 重置
记忆:--soft 最温柔(只撤销 commit 记录),--mixed 中等(撤销 commit + add),--hard 最狠(全删,不可恢复,除非用 reflog 找回)。
11.3 git revert — 创建反向提交
# 创建一个"反向提交"来撤销指定 commit
git revert 481ac6e
# 撤销最近一次提交
git revert HEAD
# 撤销多个提交
git revert HEAD~3..HEAD
TIP
📌 reset vs revert 的本质区别:
reset 是"时光倒流"——把分支指针往回移,历史中那些提交从没存在过。改写历史。
revert 是"发布补丁"——在历史末尾新增一个提交,内容是某个旧提交的反向操作。保留历史。
安全规则:
- 本地未推送的提交 → 用
reset(干净利落)
- 已推送的提交 → 用
revert(不改写历史,不会影响别人)
:::
11.4 git commit --amend — 修改最后一次提交
# 修改提交信息
git commit --amend -m "新的提交信息"
# 把遗漏的文件加入上一次提交
git add forgotten-file.txt
git commit --amend --no-edit # --no-edit 保持原提交信息不变
:::warning
🚧 --amend 会改变 commit 的 SHA-1。如果已经 push 了,再 amend 会导致本地和远程分叉,需要 --force-with-lease 推送。只对未推送的提交用 amend。
12 · 储藏与清理
12.1 git stash — 临时保存工作
# 临时保存当前工作区的改动(工作区变干净)
git stash
# 带说明信息
git stash push -m "正在写 VideoPlayer,先切去修 bug"
# 包括未跟踪的文件
git stash -u
# 查看储藏列表
git stash list
# stash@{0}: WIP on master: 481ac6e knowledge: 添加 Linux 学习笔记
# stash@{1}: WIP on master: e5b1ac1 Sync live doc template
# 恢复最近的储藏(并从列表删除)
git stash pop
# 恢复最近的储藏(但保留在列表中)
git stash apply
# 恢复指定储藏
git stash apply stash@{1}
# 删除储藏
git stash drop stash@{0}
# 清空所有储藏
git stash clear
TIP
📌 stash 的典型场景:你正在 feature 分支写新功能,写到一半时 master 上发现紧急 bug。但工作区是脏的,无法切分支。这时:
git stash — 临时存起来
git switch master — 切去修 bug
- 修完 bug,
git switch feature — 切回来
git stash pop — 恢复之前的改动,继续写
📌 pop vs apply:pop = apply + drop,恢复并删除。apply 只恢复不删除。不确定能不能恢复成功时用 apply,成功后再手动 drop。
12.2 git clean — 清除未跟踪文件
# 预览要删除哪些文件(dry run,安全检查)
git clean -n
# 删除未跟踪的文件
git clean -f
# 删除未跟踪的文件和目录
git clean -fd
# 包括 .gitignore 忽略的文件也一起删
git clean -fdx
WARNING
🚧 git clean 不可逆。被删的文件不在 Git 历史里,无法通过 reset 或 reflog 找回。先用 -n 预览,确认后再执行。
13 · 高级技巧
13.1 git cherry-pick — 摘樱桃
从某个分支"摘取"特定的 commit 到当前分支,不需要合并整个分支。
# 把 dev 分支上的某个 commit 摘到当前分支
git cherry-pick e5b1ac1
# 摘取多个 commit
git cherry-pick 481ac6e e5b1ac1
# 摘取一个范围
git cherry-pick dev~3..dev
TIP
📌 cherry-pick 的场景:你在 dev 分支上做了 5 个 commit,其中第 3 个修复了严重 bug。master 也需要这个修复,但不想把 dev 的其他 4 个 commit 也合过去。这时 cherry-pick 那一个 commit 到 master 即可。
13.2 git reflog — 操作日志(救命稻草)
# 查看所有操作记录(包括已经被 reset 掉的 commit)
git reflog
# 481ac6e HEAD@{0}: commit: knowledge: 添加 Linux 学习笔记
# e5b1ac1 HEAD@{1}: commit: Sync live doc template
# ec16745 HEAD@{2}: checkout: moving from dev to master
# ...
# 找回被 reset --hard 误删的提交
git reset --hard HEAD@{2} # 回到操作前的状态
TIP
📌 reflog 是 Git 的"后悔药"。即使你 reset --hard 丢掉了提交,那些 commit 对象并没有被立即删除(Git 会保留约 30 天才垃圾回收)。reflog 记录了 HEAD 的每一次移动,你可以从中找到丢失的 commit SHA-1,然后 reset 回去。
但前提是:commit 对象还在(30 天内)且没有被 git gc 清理。所以发现误删后越早找回越好。
13.3 git bisect — 二分查 bug
# 开始二分查找
git bisect start
# 标记当前提交为"有 bug"
git bisect bad
# 标记某个历史提交为"正常"
git bisect good 8e3db40 # 这个版本没有 bug
# Git 自动切到中间的提交,你测试后告诉 Git 结果
git bisect good # 这个版本正常
git bisect bad # 这个版本有 bug
# 重复直到 Git 定位到引入 bug 的那个 commit
# 最后结束
git bisect reset
TIP
📌 bisect 的原理:假设你有 100 个提交,不知道哪个引入了 bug。手动一个个测要 100 次。二分法每次切到中间,测一次排除一半,最多 7 次(log₂100 ≈ 7)就能定位。Git 自动帮你切换到每次的中间提交,你只需要测试并报告 good/bad。
13.4 git config — 配置
# 全局配置(写入 ~/.gitconfig)
git config --global user.name "hisos"
git config --global user.email "hisos@example.com"
git config --global core.editor "code --wait"
git config --global init.defaultBranch main # 默认分支名改为 main
# 仓库级配置(写入 .git/config,优先于全局)
git config user.name "another-name"
# 查看所有配置
git config --list
# 查看某项配置
git config user.name
# 设置别名(简化常用命令)
git config --global alias.st "status -sb"
git config --global alias.co "checkout"
git config --global alias.lg "log --oneline --graph --all"
# 之后可以 git lg 代替长命令
13.5 Git 钩子(hooks)
# 钩子脚本放在 .git/hooks/ 目录
# 常用钩子:
# pre-commit:提交前触发,可用于代码检查
# commit-msg:提交信息验证
# pre-push:推送前触发
# 示例:强制提交信息符合 Conventional Commits 规范
# .git/hooks/commit-msg
#!/bin/bash
if ! grep -qE "^(feat|fix|docs|refactor|chore|knowledge):" "$1"; then
echo "提交信息必须以 feat|fix|docs|refactor|chore: 开头"
exit 1
fi
TIP
📌 钩子是本地的:.git/hooks/ 不会被 Git 跟踪和共享。团队要共享钩子,可以把钩子放在项目的一个目录(如 scripts/hooks/),然后每个人手动 symlink 或用工具(如 husky)安装。
14 · .gitignore 配置
14.1 作用与语法
.gitignore 文件告诉 Git 哪些文件不需要跟踪。以本项目 RyBOS 的 .gitignore 为例:
# Qt 编译产生的中间文件
*.o
*.obj
moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
*.moc
*.qm
Makefile*
*build-*
# 编译目录
build/
buildCache/
debug/
release/
# 其他常见忽略文件
*.log
*.dmp
*.pdb
*.so
*.so.*
# IDE 相关文件
.vs/
.vscode/
*.swp
*~
.DS_Store
# 秘钥文件
scripts/.env
14.2 语法规则
| 模式 |
含义 |
示例 |
* |
匹配任意字符(不含 /) |
*.log — 所有 log 文件 |
** |
匹配任意路径(含 /) |
**/foo — 任意层级的 foo |
? |
匹配单个字符 |
te??.txt |
[abc] |
匹配方括号内字符 |
*.[oa] — .o 或 .a 文件 |
/ 开头 |
只匹配根目录 |
/build — 只忽略根目录的 build |
/ 结尾 |
只匹配目录 |
build/ — 只忽略目录 |
! |
取反(强制不忽略) |
!important.log — 即使 *.log 也跟踪此文件 |
# |
注释 |
# 这是注释 |
14.3 全局 .gitignore
# 设置全局忽略文件(对所有仓库生效)
git config --global core.excludesfile ~/.gitignore_global
适合放操作系统级别的忽略(如 .DS_Store、Thumbs.db),不用每个项目重复写。
14.4 已跟踪文件被 ignore 了怎么办
.gitignore 只对未跟踪的文件生效。如果一个文件已经被 Git 跟踪了,后来加到 .gitignore 里,Git 仍然会继续跟踪它。
# 解决方法:先从 Git 跟踪中移除(保留本地文件)
git rm --cached <文件>
git commit -m "chore: 停止跟踪 xxx 文件"
# 之后 .gitignore 规则才会对它生效
TIP
📌 常见误区:往 .gitignore 加了一行,但文件还是出现在 git status 里——因为它已经被跟踪了。.gitignore 是"预防针",不是"治疗药"。先 git rm --cached 取消跟踪,gitignore 才能生效。
附录 A · 命令速查手册
基础操作
| 命令 |
说明 |
git init |
初始化仓库 |
git clone <url> |
克隆远程仓库 |
git status |
查看当前状态 |
git add <file> |
添加到暂存区 |
git add . |
添加所有改动 |
git add -p |
交互式添加部分改动 |
git commit -m "msg" |
提交 |
git commit -am "msg" |
提交已跟踪文件的修改(跳过 add) |
git commit --amend |
修改最后一次提交 |
git diff |
工作区 vs 暂存区 |
git diff --cached |
暂存区 vs 版本库 |
git diff HEAD |
工作区 vs 版本库 |
git rm <file> |
删除文件 |
git rm --cached <file> |
停止跟踪但保留本地 |
git mv <old> <new> |
重命名/移动 |
查看历史
| 命令 |
说明 |
git log --oneline |
每条提交一行 |
git log --oneline --graph --all |
带分支图 |
git log -n 5 |
最近 5 条 |
git log --author="name" |
按作者过滤 |
git log --since="1 week ago" |
按日期过滤 |
git log --grep="fix" |
按提交信息过滤 |
git log --follow -- <file> |
某文件的完整历史 |
git log --stat |
显示文件变更统计 |
git log -p |
显示完整 diff |
git show <commit> |
查看某个提交详情 |
git blame <file> |
逐行追溯 |
git shortlog -sn |
统计每人提交次数 |
分支操作
| 命令 |
说明 |
git branch |
列出本地分支 |
git branch -a |
列出所有分支(含远程) |
git branch <name> |
创建分支 |
git switch <name> |
切换分支 |
git switch -c <name> |
创建并切换 |
git branch -d <name> |
安全删除(未合并拒绝) |
git branch -D <name> |
强制删除 |
git branch -m <old> <new> |
重命名 |
git merge <branch> |
合并分支 |
git merge --no-ff <branch> |
强制创建合并提交 |
git rebase <branch> |
变基 |
git rebase --abort |
放弃变基 |
git cherry-pick <commit> |
摘取单个 commit |
远程操作
| 命令 |
说明 |
git remote -v |
查看远程仓库 |
git remote add <name> <url> |
添加远程 |
git remote set-url <name> <url> |
修改远程地址 |
git fetch |
下载远程更新(不合并) |
git pull |
fetch + merge |
git pull --rebase |
fetch + rebase |
git push |
推送 |
git push -u origin <branch> |
首次推送并设置上游 |
git push --force-with-lease |
安全强制推送 |
git push origin --tags |
推送所有标签 |
撤销与回退
| 命令 |
说明 |
git restore <file> |
恢复工作区文件 |
git restore --staged <file> |
取消暂存 |
git reset --soft HEAD~1 |
撤销 commit,保留暂存区 |
git reset --mixed HEAD~1 |
撤销 commit + 暂存区 |
git reset --hard HEAD~1 |
彻底回退(危险) |
git revert <commit> |
创建反向提交(安全) |
git stash |
临时保存 |
git stash pop |
恢复并删除 |
git stash apply |
恢复但保留 |
git stash list |
查看储藏列表 |
git clean -fd |
删除未跟踪文件和目录 |
git reflog |
操作日志(找回误删) |
标签
| 命令 |
说明 |
git tag |
列出标签 |
git tag <name> |
创建轻量标签 |
git tag -a <name> -m "msg" |
创建附注标签 |
git show <tag> |
查看标签信息 |
git push origin <tag> |
推送单个标签 |
git push origin --tags |
推送所有标签 |
git tag -d <name> |
删除本地标签 |
git push origin --delete <tag> |
删除远程标签 |
配置
| 命令 |
说明 |
git config --global user.name "name" |
设置全局用户名 |
git config --global user.email "email" |
设置全局邮箱 |
git config --global core.editor "cmd" |
设置默认编辑器 |
git config --global alias.<x> "cmd" |
设置别名 |
git config --list |
查看所有配置 |
git config --global init.defaultBranch main |
设置默认分支名 |
附录 B · 常见问题速查
Q1:git push 报错 "Updates were rejected because the remote contains work..."
原因:远程有你本地没有的提交(别人 push 了)。
# 先拉取再推送
git pull --rebase
git push
Q2:不小心 git reset --hard 丢了提交,怎么找回?
git reflog # 找到丢失的 commit SHA-1
git reset --hard <sha-1> # 回到那个提交
Q3:提交信息写错了,怎么改?
# 还没 push
git commit --amend -m "正确的信息"
# 已经 push(用 amend + force-with-lease)
git commit --amend -m "正确的信息"
git push --force-with-lease
Q4:想把某个文件从 Git 历史中彻底删除
# 危险操作,会重写所有历史
git filter-branch --tree-filter 'rm -f sensitive.txt' HEAD
# 或用更快的工具
git filter-repo --path sensitive.txt --invert-paths
# 之后需要 force push
git push --force-with-lease
Q5:merge 冲突太多,想放弃
git merge --abort # 回到合并前的状态
Q6:怎么把某次提交的改动撤销但不删历史?
git revert <commit> # 创建一个反向提交
Q7:git pull 产生了很多 merge commit,历史很乱
# 改用 rebase 模式拉取
git pull --rebase
# 设为默认
git config --global pull.rebase true
Q8:怎么查看某个文件在某个历史版本的内容?
git show <commit>:path/to/file
# 或检出到工作区
git checkout <commit> -- path/to/file
Q9:clone 太慢了
# 只克隆最近一次提交
git clone --depth 1 <url>
# 克隆后需要完整历史时再补
git fetch --unshallow
Q10:怎么忽略已经被跟踪的文件?
git rm --cached <file> # 停止跟踪
echo "<file>" >> .gitignore # 加入忽略
git commit -m "chore: 停止跟踪 <file>"
附录 C · 实战场景
场景 1:从零开始一个新项目
# 1. 初始化
mkdir my-project && cd my-project
git init
git config user.name "hisos"
git config user.email "hisos@example.com"
# 2. 创建 .gitignore
cat > .gitignore << 'EOF'
build/
*.log
.vs/
.vscode/
EOF
# 3. 首次提交
git add .
git commit -m "feat: 项目初始化"
# 4. 关联远程并推送
git remote add origin git@gitee.com:hisos/my-project.git
git push -u origin master
场景 2:功能开发完整流程
# 1. 从 master 拉取最新
git switch master
git pull
# 2. 创建功能分支
git switch -c feature/video-player
# 3. 开发并提交(小步多次提交)
git add qt/VideoPlayer/src/QtVideoThread.h
git commit -m "feat: 添加 VideoThread 低延迟 FFmpeg 配置"
git add qt/VideoPlayer/src/QtGLVideoWidget.h
git commit -m "feat: 添加 GLVideoWidget GPU 渲染组件"
# 4. 开发期间同步 master 最新
git fetch origin
git rebase origin/master
# 5. 完成后合并到 master
git switch master
git merge --no-ff feature/video-player
# 6. 推送
git push
# 7. 删除功能分支
git branch -d feature/video-player
场景 3:紧急修 bug(stash 工作流)
# 正在 feature 分支写代码,工作区是脏的
# master 发现紧急 bug!
git stash push -m "WIP: video player 开发中"
git switch master
git switch -c hotfix/crc-overflow
# 修复 bug
git add cpp/CRC/src/CRC.cpp
git commit -m "fix: CRC 计算 uint16_t 溢出改用 uint32_t"
# 合并到 master
git switch master
git merge --no-ff hotfix/crc-overflow
git push
# 删除 hotfix 分支
git branch -d hotfix/crc-overflow
# 回到 feature 分支继续开发
git switch feature/video-player
git stash pop
场景 4:用 bisect 定位 bug 引入点
# 当前版本有 bug,但不知道哪个提交引入的
git bisect start
git bisect bad HEAD # 当前版本有 bug
git bisect good v0.9 # v0.9 标签的版本正常
# Git 切到中间提交,测试
git bisect good # 这个版本正常 → bug 在后半段
# Git 再切到后半段中间
git bisect bad # 这个版本有 bug → bug 在前半段
# ... 最多 7 次就能定位
# 定位到引入 bug 的 commit 后
git bisect reset # 退出 bisect 模式
git show <bug-commit> # 查看那个提交改了什么
场景 5:发布版本打标签
# 确认在 master 且代码正确
git switch master
git pull
# 创建附注标签
git tag -a v1.0 -m "发布 v1.0:包含 CRC、AsyncTask、ConfigManager 等 20 个模块"
# 推送标签到远程
git push origin v1.0
# 之后任何人都可以精确回到这个版本
git checkout v1.0