Git 学习笔记

一份完整的 Git 学习笔记。重点讲"为什么这么设计"(费曼式 📌 讲解),配实操示例。以本项目 RyBOS 的真实 Git 历史为案例,学完就能上手。

怎么用这份笔记

  1. 学习/复习 → 顺序看正文,重点看 📌 类比和"为什么"
  2. 查命令 → 翻 附录 A 速查手册
  3. 排错 → 翻 附录 B 常见问题速查
  4. 落地 → 跟 附录 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 \xb7 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 对象模型

图 2 \xb7 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 的三种境界:

  1. git diff — "我改了什么还没存"(工作区 → 暂存区)
  2. git diff --cached — "我存了什么还没提交"(暂存区 → 版本库)
  3. 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 · 分支管理

图 3 \xb7 分支与 HEAD 指针

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 · 合并与变基

图 4 \xb7 merge vs rebase 对比

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

📌 减少冲突的最佳实践:

  1. 频繁拉取:每天开始工作前 git pull
  2. 小步提交:改一点提交一点,不要攒一大堆
  3. 短命分支:功能分支尽快合并,不要拖几周
  4. 沟通协调:避免两个人同时改同一文件
  5. 模块化:不同功能在不同文件里,天然减少冲突 :::

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 · 工作流模型

图 6 \xb7 Git 工作流模型对比

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 · 撤销与回退

图 5 \xb7 撤销操作全景图

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。但工作区是脏的,无法切分支。这时:

  1. git stash — 临时存起来
  2. git switch master — 切去修 bug
  3. 修完 bug,git switch feature — 切回来
  4. 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
本页目录