Skip to content

元数据卡

  • 前置知识:第1章(文件系统)、第2章(终端基本命令)
  • 预计时间:40 分钟
  • 核心难度:(进阶)
  • 阅读模式: 高度专注
  • 完成标志:能够独立用 Git 管理个人项目,创建分支、解决冲突、提交 PR

你在哪

你还在出发前的工坊里。终端的命令已经能磕磕绊绊地敲几句了,但你很快发现一个大问题——代码改坏了就找不回来了。

工坊的墙壁上挂满了工具:终端已经会用了,文件系统也摸透了。但你很快会发现一个问题——你改了一堆代码,改到一半觉得方向不对,想回退到一小时前的状态——可是已经来不及了。文件已经被覆盖了好几次,之前的版本消失得像从来没存在过。

这个问题的名字,叫"没有后悔药"。

你的任务

你的第一个项目已经有十几个文件了。你改了一个函数,但不确定是不是改对了。你想保留现在的版本,同时也想留有修改前的版本。甚至,你想让另一个人也能同时改这些代码,最后把两个人的工作合到一起。你需要一样东西——一个能让时间分叉、又能合并回来的工具。你需要一台时光机

Git,就是这台时光机。

本章分层

  • 必读git initgit statusgit addgit commitgit loggit branchgit switchgit mergegit pullgit push,以及一次简单的合并冲突处理
  • 选读git rebase 整理提交历史、git stash 暂存工作、远程仓库协作与 Pull Request
  • 深水区:Git Flow 工作流模型、git rebase --onto 高级历史重写、Git 底层对象模型

本章不会要求你掌握

  • git rebase 的完整用法——团队协作进阶才需要
  • Git Flow 严格模型——对个人或小团队开发过于复杂
  • git bisect 二分查找 bug——等你的项目足够大了再回来看

遭遇战 → 获得技能

第一场战斗:改错了,想回去

你对着工坊的图纸修修改改了一整个下午。现在你盯着案台上的设计图,心里有点发毛——因为刚才手滑,墨水瓶打翻了,把整张关键的图纸洇成了一片。

"等等,我……"你拿布想擦一下,结果越擦越糟——墨迹糊开了,线条全花了。图纸已经毁了。再落一笔会怎样?只会更糟。

你开始意识到一个残酷的事实:一张设计图一旦被毁掉,之前的方案就彻底消失了。没有涂改液能跨过'已经毁了'这道坎。

"要是能回到一盏茶之前就好了。"

你走到工坊的材料架前,在你的项目柜子里翻找。你在铜板上刻了一些花纹标记,放进锻造炉烧了一次——结果淬火时裂了。你试着补一补——这次更糟,整块材料断成了两截。你心想:"要是能回到一盏茶之前就好了。"

这时候你发现终端里有个命令没用过:

bash
# 先看看工坊里有没有时光机
cd ~/project/my-first-project
git status

语言: Bash 如何运行: 在终端项目目录中执行 预期输出: fatal: not a git repository (or any of the parent directories)

果然,没有。你得先装一台。


第一次初始化

bash
# 在你的项目里装一台时光机
cd ~/project/my-first-project
git init

语言: Bash 如何运行: 在终端中执行 预期输出: Initialized empty Git repository in /home/你/project/my-first-project/.git/

看到 Initialized 了吗?Git 在你的项目根目录里创建了一个隐藏文件夹 .git。现在这台时光机已经装上了,只不过里面还没有任何存档。


第一次存档

你改好了 main.py,觉得这版本值得保存:

bash
# 看看哪些文件变了
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.md

Git 说:你有文件还没被跟踪。在你的项目里,这些文件就像是散落在工坊地上的材料——Git 知道它们存在,但还没拿起来放进仓库。

你需要把它们先捡起来,放到一个"准备台"上:

bash
# 把文件放到暂存区(准备台)
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

确定没问题了,你就按下"保存"按钮:

bash
# 创建一个存档点(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 最值钱的功能——时光倒流:

bash
# 看看你的存档历史
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.pygit checkout -- main.py

Git 的三棵树的模型

你看到的并不是什么魔法,是三个区域之间的状态流转:

 工作区 (Working Directory)
   │  git add

 暂存区 (Staging Area / Index)
   │  git commit

 仓库 (Repository / .git)

你在工作区里敲代码→觉得差不多了→git add 放到暂存区→确认没漏→git commit 存进仓库。仓库里的每个 commit 都是一个"完整快照"——Git 不存"差异",它存的是完整文件(结合压缩算法)。


第二场战斗:我开始做多个实验

你有了存档功能,心里踏实了。但有一个新问题出现了:你想试试一个更大胆的方案——尝试一种全新的锻造工艺。

可是你不敢。你怕把现在这把已经锻好的剑坯砸坏了,然后整件作品都回不来。你现在的成品虽然不算完美但勉强能用,万一新工艺试了不行,连现在的都找不回来。

"要是能有两个平行的工作台就好了,"你自言自语,"一个台子上我继续维护当前这版成品,另一个台子上我随便折腾新材料,就算炸了炉也不影响。"

项目变大了。你有个想法:想试试不同的打造方案。但你不想在现在这张设计图上直接改——万一改坏了,连现在这版能用的方案都没了。

这时候你需要分支。分支就像时光机上的"平行宇宙开关"。

bash
# 创建一个叫 experiment 的分支
git branch experiment

# 切换到 experiment 分支
git switch experiment

# 上面两条命令等价于下面这一条(创建并切换)
git switch -c experiment

语言: Bash 如何运行: 在项目目录中执行 预期输出(第一条命令): 无输出,创建成功 验证: git branch 列出所有分支,当前分支前面有 *

  master
* experiment

现在你在 experiment 分支上改代码。改了多少行、删了多少文件,master 分支纹丝不动。它们是两个平行的世界。

bash
# 在 experiment 分支上改东西
echo "print('hello from experiment')" >> main.py
git add main.py
git commit -m "experiment: 添加新的打印逻辑"

改完实验后,你想回 master 看看:

bash
git switch master

回到 master 后发现——main.py 没有刚才那行 print。它完好无损地保留在你离开时的状态。

这就是 Git 分支的意义: 你可以同时进行多个互不干扰的探索,每个探索是一个独立的时间线。


第三场战斗:想把实验成果合并回来

你在实验分支上改了一大堆东西——新锻造工艺验证通过了,之前发现的铸造缺陷也修好了,所有样品都通过了测试。

现在问题来了:你在实验分支上做的所有改进,怎么变回主干设计方案的一部分?你不可能把一整份新图纸手抄一遍,重新在 master 设计稿上画一遍。

"你画了两版图纸,"工坊主人不知何时站在你背后,"现在你想把第二版的改进加到第一版上。怎么做?"

实验成功了——你想把 experiment 分支的代码合并到 master 里。这就叫合并

bash
# 在 master 分支上
git merge experiment

语言: Bash 预期输出(顺利的情况):

Updating a1b2c3d..e5f6g7h
Fast-forward
 main.py | 1 +
 1 file changed, 1 insertion(+)

看到 Fast-forward 了吗?这表示 master 自从创建 experiment 分支后没改过任何东西——所以 Git 只需要把 experiment 上的提交"快进"过来就行。这是最简单的情况。

但如果两个分支在同一个地方都改了东西呢?——这才叫真正的挑战。


第四场战斗:两个人改了同一行

前面几次合并都挺顺利的——你只是往主干设计稿上加新的零件图,跟已有的方案不冲突。但这次不一样了。

你在自己的工位上改了一个零件的尺寸,隔壁工位的同事也在他的工位上改了同一个零件的尺寸。你俩的改动方向完全相反。

"合到一起试试?"你的同事说。

你把两份图纸放上操作台,运行了比对。操作台沉默了一秒——然后弹出了一行红色的标记:

冲突

"什么叫冲突?"你有点慌了。

你和同事同时修改了同一张图纸上的同一处尺寸。你说"这个接口要用圆形",他说"要改成方形"。你们各自把版本登记进了工坊日志。现在你要把两个设计合到一起——操作台困惑了:"你俩到底谁对?"

这就是合并冲突

制造一个冲突看看:

bash
# 我从 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

bash
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 给你留了标记:

python
<<<<<<< HEAD
b
=======
a
>>>>>>> feature-b

这是 Git 在说:"======= 上面的是当前分支(master,现在包含了 feature-a 的改动)的版本,下面的是你要合并进来的版本(feature-b 的版本)。你自己选一个,或者重写。"

你要做的就是:

  1. 删掉 <<<<<<<=======>>>>>>> 这些标记
  2. 改成你想要的内容
  3. 保存文件
  4. git add 然后 git commit
bash
# 假设我决定两边都要
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

bash
# 把本地仓库和远程仓库关联起来
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 就知道推到哪了"。

别人想拉你的代码:

bash
git clone https://github.com/你的用户名/my-project.git
cd my-project

第六场战斗:别人推了新代码,你想同步

你睡了一觉,第二天回到工坊,准备继续你未完成的作品。但你习惯性地先翻开共享的工坊日志本——发现隔壁工位的同事昨天新登记了十几次设计修改。

你的本地设计稿还是昨天早上的状态。你画的图纸和同事画的图纸现在就像是两条不同的锻造路线——你的路走通了,他的路也走通了,但两条路线的终点完全不同。

你现在不能直接把你的修改登记到共享日志上——因为你手头的版本已经落后了。你需要一个办法,把别人登记的更新取下来,跟你的改动合并在一起。

你在本地改代码的时候,你的同事已经把新代码推到了远程仓库。你的本地仓库已经落后了。你需要把别人的代码拉下来,再跟你的合并——这就是 git pull

bash
# 拉取远程最新代码并合并到本地
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(把下载的提交合并到你当前分支)。

如果你不想自动合并,可以拆开做:

bash
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

bash
# 假设你在 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 → master
  • master 永远是稳定的、可部署的
  • 任何修改都在独立分支上完成
  • 通过 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-branchBFG Repo-Cleaner 才能从历史中彻底清除。或者更聪明的办法:从一开始就用 .gitignoreGit LFS(Git Large File Storage)管理大文件。

通关挑战

🗡 热身(5 分钟,必做)

  1. 在你电脑任意目录运行 git init,创建一个仓库
  2. 新建一个 hello.txt 文件,写入你名字,git add 然后 git commit
  3. 修改 hello.txt,再 commit 一次
  4. 运行 git log --oneline,确认看到两次提交
  5. 运行 git diff HEAD~1 HEAD,看看两次提交之间的差异

挑战(30 分钟,选做)

  1. 创建一个 test-branch 分支,在上面修改 hello.txt
  2. 切换回 master,修改 hello.txt同一行为不同内容
  3. 尝试合并 test-branchmaster,看冲突出现
  4. 手动解决冲突并 commit
  5. 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(已推送到共享远程的分支)——这是进阶内容,先记住这条规则

常见卡点

  1. "工作区、暂存区、仓库什么区别?"

    • 工作区是你修改文件的地方,暂存区是你"确定要这些文件"的地方,仓库是存档的地方。每次 git commit 只存暂存区里的内容,工作区不管。
  2. "git reset、git restore、git checkout 分不清"

    • 这是 Git 历史上最混乱的三个命令。在新版 Git 里用 git restore 来撤销改动,用 git switch 来切换分支,用 git reset 来回退 commit。不要用 git checkout 做这两件事——它只是历史原因留下的老接口。
  3. "为什么我的合并冲突总是出现 <<<<<<<"

    • 别慌,那是 Git 在告诉你哪些行有冲突。删掉标记、改正确代码、add 并 commit。冲突不是灾难,是 Git 诚实地说"我没法替你决定"。
  4. "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 怎么办?你盯着屏幕上闪动的光标,看着代码一行行滚过——"有没有一种工具,能让代码在运行的过程中停下来,让我看看它里面到底在发生什么?"

有。下一章,你的显微镜。

Built with VitePress | Software Systems Atlas