元数据卡
- 前置知识:第1章(文件系统)、第2章(终端基本命令)
- 预计时间:40 分钟
- 核心难度:(进阶)
- 阅读模式: 高度专注
- 完成标志:能够独立用 Git 管理个人项目,创建分支、解决冲突、提交 PR
你在哪
你还在出发前的工坊里。终端的命令已经能磕磕绊绊地敲几句了,但你很快发现一个大问题——代码改坏了就找不回来了。
工坊的墙壁上挂满了工具:终端已经会用了,文件系统也摸透了。但你很快会发现一个问题——你改了一堆代码,改到一半觉得方向不对,想回退到一小时前的状态——可是已经来不及了。文件已经被覆盖了好几次,之前的版本消失得像从来没存在过。
这个问题的名字,叫"没有后悔药"。
你的任务
你的第一个项目已经有十几个文件了。你改了一个函数,但不确定是不是改对了。你想保留现在的版本,同时也想留有修改前的版本。甚至,你想让另一个人也能同时改这些代码,最后把两个人的工作合到一起。你需要一样东西——一个能让时间分叉、又能合并回来的工具。你需要一台时光机。
Git,就是这台时光机。
本章分层
- 必读:
git init、git status、git add、git commit、git log、git branch、git switch、git merge、git pull、git push,以及一次简单的合并冲突处理- 选读:
git rebase整理提交历史、git stash暂存工作、远程仓库协作与 Pull Request- 深水区:Git Flow 工作流模型、
git rebase --onto高级历史重写、Git 底层对象模型本章不会要求你掌握
git rebase的完整用法——团队协作进阶才需要- Git Flow 严格模型——对个人或小团队开发过于复杂
git bisect二分查找 bug——等你的项目足够大了再回来看
遭遇战 → 获得技能
第一场战斗:改错了,想回去
你对着工坊的图纸修修改改了一整个下午。现在你盯着案台上的设计图,心里有点发毛——因为刚才手滑,墨水瓶打翻了,把整张关键的图纸洇成了一片。
"等等,我……"你拿布想擦一下,结果越擦越糟——墨迹糊开了,线条全花了。图纸已经毁了。再落一笔会怎样?只会更糟。
你开始意识到一个残酷的事实:一张设计图一旦被毁掉,之前的方案就彻底消失了。没有涂改液能跨过'已经毁了'这道坎。
"要是能回到一盏茶之前就好了。"
你走到工坊的材料架前,在你的项目柜子里翻找。你在铜板上刻了一些花纹标记,放进锻造炉烧了一次——结果淬火时裂了。你试着补一补——这次更糟,整块材料断成了两截。你心想:"要是能回到一盏茶之前就好了。"
这时候你发现终端里有个命令没用过:
# 先看看工坊里有没有时光机
cd ~/project/my-first-project
git status语言: Bash 如何运行: 在终端项目目录中执行 预期输出: fatal: not a git repository (or any of the parent directories)
果然,没有。你得先装一台。
第一次初始化
# 在你的项目里装一台时光机
cd ~/project/my-first-project
git init语言: Bash 如何运行: 在终端中执行 预期输出: Initialized empty Git repository in /home/你/project/my-first-project/.git/
看到 Initialized 了吗?Git 在你的项目根目录里创建了一个隐藏文件夹 .git。现在这台时光机已经装上了,只不过里面还没有任何存档。
第一次存档
你改好了 main.py,觉得这版本值得保存:
# 看看哪些文件变了
git status语言: Bash 如何运行: 在项目目录中执行 预期输出:
On branch master
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
main.py
README.mdGit 说:你有文件还没被跟踪。在你的项目里,这些文件就像是散落在工坊地上的材料——Git 知道它们存在,但还没拿起来放进仓库。
你需要把它们先捡起来,放到一个"准备台"上:
# 把文件放到暂存区(准备台)
git add main.py README.md
# 或者用捷径:把所有变化都加进去
git add .语言: Bash 如何运行: 执行后,再跑一次 git status预期输出:
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: main.py
new file: README.md现在它们被放在"暂存区"——这个位置就是你确认"我确实想存档这些文件"的地方。你还可以反悔,从暂存区拿掉某个文件:git rm --cached main.py。
确定没问题了,你就按下"保存"按钮:
# 创建一个存档点(commit)
git commit -m "我的第一个项目:初始化 main.py 和 README"语言: Bash 预期输出:
[master (root-commit) a1b2c3d] 我的第一个项目:初始化 main.py 和 README
2 files changed, 15 insertions(+)
create mode 100644 main.py
create mode 100644 README.md恭喜——你创建了第一个 commit。那串 a1b2c3d 是这次存档的唯一编号(哈希值)。从现在开始,你随时可以回到这一刻。
回到过去
你继续改 main.py,改了一堆,发现方向错了。这是 Git 最值钱的功能——时光倒流:
# 看看你的存档历史
git log
# 回到上一个存档(放弃所有未提交的修改)
git checkout -- .语言: Bash 预期输出(git log):
commit a1b2c3d4e5f6... (HEAD -> master)
Author: 你 <你@example.com>
Date: Mon Jun 23 16:00:00 2026 +0800
我的第一个项目:初始化 main.py 和 README注意:
git checkout -- .会永久放弃所有未提交的修改,无法撤销。除非你很确定不再需要这些改动,否则先用git stash暂存它们。新版 Git 推荐用git restore .,语义更清晰:它只做"恢复"一件事,不像checkout又管分支又管文件。
git restore . 或 git checkout -- . 把你工作区里所有未提交的修改都撤销了,文件恢复成最近一次 commit 的状态。想恢复某个文件而不是全部?用 git restore main.py 或 git checkout -- main.py。
Git 的三棵树的模型
你看到的并不是什么魔法,是三个区域之间的状态流转:
工作区 (Working Directory) │ git add ▼ 暂存区 (Staging Area / Index) │ git commit ▼ 仓库 (Repository / .git)你在工作区里敲代码→觉得差不多了→
git add放到暂存区→确认没漏→git commit存进仓库。仓库里的每个 commit 都是一个"完整快照"——Git 不存"差异",它存的是完整文件(结合压缩算法)。
第二场战斗:我开始做多个实验
你有了存档功能,心里踏实了。但有一个新问题出现了:你想试试一个更大胆的方案——尝试一种全新的锻造工艺。
可是你不敢。你怕把现在这把已经锻好的剑坯砸坏了,然后整件作品都回不来。你现在的成品虽然不算完美但勉强能用,万一新工艺试了不行,连现在的都找不回来。
"要是能有两个平行的工作台就好了,"你自言自语,"一个台子上我继续维护当前这版成品,另一个台子上我随便折腾新材料,就算炸了炉也不影响。"
项目变大了。你有个想法:想试试不同的打造方案。但你不想在现在这张设计图上直接改——万一改坏了,连现在这版能用的方案都没了。
这时候你需要分支。分支就像时光机上的"平行宇宙开关"。
# 创建一个叫 experiment 的分支
git branch experiment
# 切换到 experiment 分支
git switch experiment
# 上面两条命令等价于下面这一条(创建并切换)
git switch -c experiment语言: Bash 如何运行: 在项目目录中执行 预期输出(第一条命令): 无输出,创建成功 验证: git branch 列出所有分支,当前分支前面有 *
master
* experiment现在你在 experiment 分支上改代码。改了多少行、删了多少文件,master 分支纹丝不动。它们是两个平行的世界。
# 在 experiment 分支上改东西
echo "print('hello from experiment')" >> main.py
git add main.py
git commit -m "experiment: 添加新的打印逻辑"改完实验后,你想回 master 看看:
git switch master回到 master 后发现——main.py 没有刚才那行 print。它完好无损地保留在你离开时的状态。
这就是 Git 分支的意义: 你可以同时进行多个互不干扰的探索,每个探索是一个独立的时间线。
第三场战斗:想把实验成果合并回来
你在实验分支上改了一大堆东西——新锻造工艺验证通过了,之前发现的铸造缺陷也修好了,所有样品都通过了测试。
现在问题来了:你在实验分支上做的所有改进,怎么变回主干设计方案的一部分?你不可能把一整份新图纸手抄一遍,重新在 master 设计稿上画一遍。
"你画了两版图纸,"工坊主人不知何时站在你背后,"现在你想把第二版的改进加到第一版上。怎么做?"
实验成功了——你想把 experiment 分支的代码合并到 master 里。这就叫合并。
# 在 master 分支上
git merge experiment语言: Bash 预期输出(顺利的情况):
Updating a1b2c3d..e5f6g7h
Fast-forward
main.py | 1 +
1 file changed, 1 insertion(+)看到 Fast-forward 了吗?这表示 master 自从创建 experiment 分支后没改过任何东西——所以 Git 只需要把 experiment 上的提交"快进"过来就行。这是最简单的情况。
但如果两个分支在同一个地方都改了东西呢?——这才叫真正的挑战。
第四场战斗:两个人改了同一行
前面几次合并都挺顺利的——你只是往主干设计稿上加新的零件图,跟已有的方案不冲突。但这次不一样了。
你在自己的工位上改了一个零件的尺寸,隔壁工位的同事也在他的工位上改了同一个零件的尺寸。你俩的改动方向完全相反。
"合到一起试试?"你的同事说。
你把两份图纸放上操作台,运行了比对。操作台沉默了一秒——然后弹出了一行红色的标记:
冲突
"什么叫冲突?"你有点慌了。
你和同事同时修改了同一张图纸上的同一处尺寸。你说"这个接口要用圆形",他说"要改成方形"。你们各自把版本登记进了工坊日志。现在你要把两个设计合到一起——操作台困惑了:"你俩到底谁对?"
这就是合并冲突。
制造一个冲突看看:
# 我从 master 创建 feature-a 分支
git switch -c feature-a
# 在 main.py 第 1 行改成 "a"
echo "a" > main.py
git add main.py && git commit -m "feature-a: change to a"
# 回到 master,创建 feature-b 分支
git switch master
git switch -c feature-b
# 在 main.py 第 1 行改成 "b"
echo "b" > main.py
git add main.py && git commit -m "feature-b: change to b"现在回到 master,先合并 feature-a,再合并 feature-b:
git switch master
git merge feature-a # 顺利,因为 master 没改过 main.py
git merge feature-b # 冲突!两个分支都改了同一行预期输出:
Auto-merging main.py
CONFLICT (content): Merge conflict in main.py
Automatic merge failed; fix conflicts and then commit the result.你打开 main.py,会看到 Git 给你留了标记:
<<<<<<< HEAD
b
=======
a
>>>>>>> feature-b这是 Git 在说:"======= 上面的是当前分支(master,现在包含了 feature-a 的改动)的版本,下面的是你要合并进来的版本(feature-b 的版本)。你自己选一个,或者重写。"
你要做的就是:
- 删掉
<<<<<<<、=======、>>>>>>>这些标记 - 改成你想要的内容
- 保存文件
git add然后git commit
# 假设我决定两边都要
echo -e "a\nb" > main.py
git add main.py
git commit -m "resolve merge conflict: keep both a and b"冲突解决完成。
核心原则: Git 不会替你解决语义上的冲突("a 还是 b 对?"),它只解决文本上的冲突("这行有两个版本")。语义合并是你——作为人——的工作。
第五场战斗:想跟世界分享你的代码
你在自己的工坊里用 Git 已经用得越来越顺了。但你有几个朋友在另外的工坊里——他们也想看你锻造的成品和设计图。
"你把图纸誊抄一份送过来吧?"你的朋友说。
"不行,"你摇头,"我改完了他又得重新誊抄一整份。而且我俩各自改的地方肯定会打架。"
"那怎么办?"
你需要一个地方——不在你的工坊里,但在所有人之间——一个所有人都能访问的共享仓库。
你一个人在本地的工坊里用 Git 很爽。但终究有一次,你需要把设计图和方案放到一个大家都能取得到的地方——让别人能拿走改进、能提交修改建议、能在你不在的时候继续你的工作。
这就是远程仓库的意义。最常见的仓库是 GitHub/GitLab/Gitee。
你在 GitHub 上创建了一个叫 my-project 的仓库。GitHub 会给你一个地址,比如 https://github.com/你的用户名/my-project.git。
# 把本地仓库和远程仓库关联起来
git remote add origin https://github.com/你的用户名/my-project.git
# 把你的代码推上去
git push -u origin master语言: Bash 预期输出:
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
...
To https://github.com/你的用户名/my-project.git
* [new branch] master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.origin 是远程仓库的默认别名。-u 的意思是"记住这次关联,以后 git push 就知道推到哪了"。
别人想拉你的代码:
git clone https://github.com/你的用户名/my-project.git
cd my-project第六场战斗:别人推了新代码,你想同步
你睡了一觉,第二天回到工坊,准备继续你未完成的作品。但你习惯性地先翻开共享的工坊日志本——发现隔壁工位的同事昨天新登记了十几次设计修改。
你的本地设计稿还是昨天早上的状态。你画的图纸和同事画的图纸现在就像是两条不同的锻造路线——你的路走通了,他的路也走通了,但两条路线的终点完全不同。
你现在不能直接把你的修改登记到共享日志上——因为你手头的版本已经落后了。你需要一个办法,把别人登记的更新取下来,跟你的改动合并在一起。
你在本地改代码的时候,你的同事已经把新代码推到了远程仓库。你的本地仓库已经落后了。你需要把别人的代码拉下来,再跟你的合并——这就是 git pull。
# 拉取远程最新代码并合并到本地
git pull origin master语言: Bash 预期输出(顺利的情况):
remote: Enumerating objects: 3, done.
...
Updating a1b2c3d..f6g7h8i
Fast-forward
utils.py | 10 ++++++++++
1 file changed, 10 insertions(+)git pull = git fetch(把远程的新提交下载到本地) + git merge(把下载的提交合并到你当前分支)。
如果你不想自动合并,可以拆开做:
git fetch origin
git log --oneline master..origin/master # 看看别人提交了什么
git merge origin/master # 确认没问题再合并🏔 进阶: 以下两个战斗(rebase、PR)不是个人开发者的日常。读完主线(到第六场战斗)就能管理好自己的代码了。等需要团队协作时再回来。
第七场战斗:想整理一下 Git 历史
你在实验分支上的新武器终于完成了。你兴冲冲地翻开工坊日志本,准备登记你的成果——然后你看到了你的修改记录:
a1b2c3d 调试输出
d4e5f6g 修复拼写错误
g7h8i9j 临时保存
h0i1j2k 再试一次
l3m4n5o 实际实现的功能
p6q7r8s 改回来了"这也太难看了吧……"你嘀咕着。六条提交,真正有用的就一条。其他人看你的 PR 时,会看到一堆"调试输出"和"再试一次"。
你的分支上有好几个 commit,但有一些是"调试用的""临时提交"之类的垃圾信息。你想把这几条垃圾 commit 压缩成一条干净的历史——用 rebase。
# 假设你在 feature 分支上,想合并最近 3 个 commit
git rebase -i HEAD~3语言: Bash 如何运行: 执行后会打开编辑器(通常是 vim/nano),里面显示:
pick a1b2c3d 调试输出
pick d4e5f6g 修复拼写错误
pick g7h8i9j 实际实现的功能把前两个 pick 改成 squash(简写 s):
pick a1b2c3d 调试输出
s d4e5f6g 修复拼写错误
s g7h8i9j 实际实现的功能保存退出。Git 会提示你写一个新的 commit message:
实现新功能
# 这是 3 个 commit 的合并结果结果: 原来 3 个乱糟糟的 commit 变成了 1 个干净的。
** Rebase 的黄金法则:** 永远不要 rebase 已经推送到共享远程仓库的分支。因为 rebase 会重写提交历史——别人已经基于旧历史做了工作,你重写历史会让他们崩溃。
第八场战斗:想给别人的项目提修改建议
你在工坊的图鉴里找到了一个开源设计——一把你在关注的远程工坊里的精巧工具。它有一个小缺陷,你已经知道怎么改了。
你复制了一份那个设计的图纸,改好了方案,把新图纸挂到了自己的材料架上。然后你发现一个问题:你不能把修改直接写回原设计者的工坊日志本里。
"那我怎么把我的改进给他?"你挠头。托人捎口信带一份手抄图纸?那是上个时代的做法了。
GitHub 上有一个明确的答案:你不能直接推——但你可以请求他把你改动拉进去。
你想给一个开源项目加个功能。但你不是那个项目的维护者——你不能直接往上推代码。这时候你需要 Pull Request(PR,在 GitLab 上叫 Merge Request)。
流程是这样的:
1. Fork(在 GitHub 上复制一份项目到你的账号)
2. Clone(拉到本地)
3. 创建分支(git switch -c fix-bug-123)
4. 修改代码、commit、push
5. 在 GitHub 上点击 "New Pull Request"当你点了 New Pull Request 后,GitHub 会看到:
base: original/main ← 你要合并到的目标分支
compare: 你的用户名/fix-bug-123 ← 你的修改维护者收到你的 PR 后,会在 GitHub 上:
- 查看代码变更(Files changed tab)
- 逐行留言评论
- 要求你修改(你改完 push,PR 自动更新)
- 最终合并(Merge pull request)
PR 是 Git 工作流的核心。 它不仅仅是代码合并——它是一次代码评审(Code Review),是团队协作的基本单元。
🏔 进阶:Git 的工作流模型
单人开发可以随意分支。但在团队中,你需要一个可预测的分支策略。下面是最常用的两种:
以下内容在你还是一人开发时不需要掌握。等团队协作、需要规范分支策略时再回来看。
GitHub Flow
最简单的协作模型:
master(始终可部署)
│
└── feature-A ← 在这里改,改完提 PR → master
└── fix-bug-1 ← 修完提 PR → mastermaster永远是稳定的、可部署的- 任何修改都在独立分支上完成
- 通过 PR 合并到
master - 适合持续部署的项目
Git Flow(更严格的模型)
适用于有正式发布周期的项目:
master(线上版本)
│
└── develop(日常开发)
│
├── feature/login(新功能开发)
│
└── release/1.2(发布准备,只修 bug 不加功能)日常操作速查
| 场景 | 命令 |
|---|---|
| 查看当前状态 | git status |
| 查看提交历史 | git log --oneline --graph |
| 暂存改动 | git add <file> |
| 撤销暂存 | git restore --staged <file> |
| 撤销工作区改动 | git restore <file> |
| 创建并切换分支 | git switch -c <branch> |
| 查看所有分支 | git branch -a |
| 合并另一个分支 | git merge <branch> |
| 放弃合并冲突 | git merge --abort |
| 暂时放下手头工作 | git stash |
| 恢复暂存的工作 | git stash pop |
常见陷阱
故事一:.gitignore 救我一命
我第一次把 Python 项目 push 到 GitHub,发现 .pyc 文件和 __pycache__ 目录被一起推上去了。问题不大,但每次 git status 都看到一堆无意义的缓存文件。更麻烦的是——如果有人拉下去在 Mac 上跑,Mac 会生成别的缓存,这些文件在你的 PR review 里会出现"增加了一堆二进制文件"的噪音。
解决方案是创建一个 .gitignore 文件:
__pycache__/
*.pyc
.env
node_modules/
.DS_Store把这个文件放进你的项目根目录,Git 会自动忽略列出的文件。
故事二:大文件灾难
有人把一个 500MB 的数据集 commit 进了 Git 仓库。从此每次 git pull 都要下载 500MB——哪怕后来删掉了,这 500MB 的历史记录永远存在于 .git 对象存储中。Git 不会偷懒只存"差异",它会存每一次的快照。
如果不小心干了这种事,用 git filter-branch 或 BFG Repo-Cleaner 才能从历史中彻底清除。或者更聪明的办法:从一开始就用 .gitignore 和 Git LFS(Git Large File Storage)管理大文件。
通关挑战
🗡 热身(5 分钟,必做)
- 在你电脑任意目录运行
git init,创建一个仓库 - 新建一个
hello.txt文件,写入你名字,git add然后git commit - 修改
hello.txt,再 commit 一次 - 运行
git log --oneline,确认看到两次提交 - 运行
git diff HEAD~1 HEAD,看看两次提交之间的差异
挑战(30 分钟,选做)
- 创建一个
test-branch分支,在上面修改hello.txt - 切换回
master,修改hello.txt的同一行为不同内容 - 尝试合并
test-branch到master,看冲突出现 - 手动解决冲突并 commit
- 用
git log --graph --oneline --all查看分支图
🪲 排障
场景 1: 你执行 git pull 得到报错:
error: Your local changes to the following files would be overwritten by merge诊断: 你有未 commit 的本地修改,和远程修改冲突。 解决: 要么 git stash 暂存工作,pull 完再 git stash pop;要么把你的改动先 commit。
场景 2: 你发现上次 commit 忘记包含一个重要文件。
解决:git commit --amend这会修改最近一次 commit,可以把漏掉的文件加进去。注意:如果已经推送到远程,需要 git push --force(但要确保是 solo 分支)。
验收标准
- [ ] 能解释 Git 三棵树模型(工作区 ↔ 暂存区 ↔ 仓库)
- [ ] 能独立创建仓库、commit、push、pull
- [ ] 能创建和切换分支
- [ ] 能解决一个合并冲突
- [ ] 知道
git pull = git fetch + git merge - [ ] 知道什么时候不该用 rebase(已推送到共享远程的分支)——这是进阶内容,先记住这条规则
常见卡点
"工作区、暂存区、仓库什么区别?"
- 工作区是你修改文件的地方,暂存区是你"确定要这些文件"的地方,仓库是存档的地方。每次
git commit只存暂存区里的内容,工作区不管。
- 工作区是你修改文件的地方,暂存区是你"确定要这些文件"的地方,仓库是存档的地方。每次
"git reset、git restore、git checkout 分不清"
- 这是 Git 历史上最混乱的三个命令。在新版 Git 里用
git restore来撤销改动,用git switch来切换分支,用git reset来回退 commit。不要用git checkout做这两件事——它只是历史原因留下的老接口。
- 这是 Git 历史上最混乱的三个命令。在新版 Git 里用
"为什么我的合并冲突总是出现 <<<<<<<"
- 别慌,那是 Git 在告诉你哪些行有冲突。删掉标记、改正确代码、add 并 commit。冲突不是灾难,是 Git 诚实地说"我没法替你决定"。
"git push 被拒绝了怎么办"
- 通常是你的本地版本落后于远程。先
git pull(或git fetch+git merge)同步,再git push。
- 通常是你的本地版本落后于远程。先
现在不需要理解
- Git 底层对象模型(blob、tree、commit、tag——这些是 Git 内部存储的数据结构,理解好三棵树就够用)
git rebase --onto(高级历史重写)- Git LFS、submodule、subtree
- 二分查找 bug(
git bisect)——等你项目足够大了再回来看
旅人笔记
Git 不是文件的备份系统——它是不同时间线的管理工具。分支让你同时探索多条路径,合并让你把成果收回来,冲突是 Git 诚实的表现:它知道自己不擅长替你做决定。
→ 下一站预告
代码写出来了,用 Git 管理起来了。但万一代码有 bug 怎么办?你盯着屏幕上闪动的光标,看着代码一行行滚过——"有没有一种工具,能让代码在运行的过程中停下来,让我看看它里面到底在发生什么?"
有。下一章,你的显微镜。