0%
S
🏁
🎉 Gotcha!
L o a d i n g . . .

Git 扫盲 — 从基础到规范再到进阶

前言

大多数人用 Git 只停留在 add commit push pull 四板斧。代码能交上去,但出了问题不知道怎么查,协作起来没有规范。

本文从基础概念讲起,说清楚 Git 到底在干什么;然后给出一套可以直接用的团队协作规范;最后聊一些能提升效率的冷门技巧


一、Git 是做什么的

版本控制解决什么问题

想象一下你写论文的过程:

  • 第一版、第二版、第三版、定稿版、最终版、最终版2……
  • 改了半天想找回昨天写的版本,但早就覆盖了
  • 和同学合作写文档,互相发压缩包,最后分不清谁的是最新的

版本控制系统就是来解决这些问题的。你只需要记住三个要点:

  1. 记录历史 — 每一次修改都会被记录下来,随时可以回头看或者回退
  2. 多人协作 — 每个人改各自的,最后合并到一起
  3. 分支并行 — 可以同时在多个方向开发,互不干扰

Git 的特点

Git 是目前使用最广泛的版本控制系统,由 Linux 之父 Linus Torvalds 于 2005 年创建。它的核心特点是分布式——每个本地仓库都有完整的历史记录,离线也能提交,服务器挂了也能恢复。

与之对比的是集中式系统(SVN、CVS),所有历史都存放在中央服务器,服务器一挂就全丢,网络断了就没法干活。

数据的三种状态

理解 Git 最关键的是知道文件在 Git 中会处于三种状态

状态 含义 所在位置
已修改 (modified) 改了文件,但还没保存到 Git 工作区
已暂存 (staged) 把修改标记为”待提交” 暂存区
已提交 (committed) 修改已保存到 Git 仓库 本地仓库

这三个状态对应 Git 的三个区域:

Git 三个区域:工作区、暂存区、版本库

文件在三个区域之间的流转,就是 Git 最基本的操作。


二、Git 基础工作流

核心命令一览

下面这张表概括了最常用的 Git 命令,以及它们把数据从哪送到哪:

命令 作用 数据流向
git status 查看当前文件的所处状态
git add <file> 将修改存入暂存区 工作区 → 暂存区
git commit -m "msg" 将暂存区的内容提交成 commit 暂存区 → 本地仓库
git push 将本地提交推送到远程 本地仓库 → 远程仓库
git pull 从远程拉取最新代码 远程仓库 → 工作区
git log 查看提交历史
git diff 查看具体改了什么内容

文件的状态转换

在 Git 中,一个文件可能处于以下状态之一:

Git 文件生命周期

也就是说:

  • 新建的文件(Untracked)→ git add → 暂存(Staged)
  • 已跟踪的文件修改后(Modified)→ git add → 暂存(Staged)
  • 暂存区的内容(Staged)→ git commit → 成为一次 commit

一个完整的示例

从头到尾走一遍最基本的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 初始化仓库或克隆已有的
git init
# git clone https://github.com/user/repo.git

# 查看当前状态
git status

# 添加文件到暂存区
git add README.md

# 再次查看状态(会看到文件变成绿色,表示已暂存)
git status

# 把暂存区的内容提交
git commit -m "docs: add README"

# 查看提交历史
git log --oneline

# 如果配置了远程仓库,推送到远程
git push origin main

# 从远程拉取最新代码
git pull origin main

查看修改内容

git status 告诉你”改了什么文件”,git diff 告诉你”具体改了哪些行”:

1
2
3
4
5
6
7
8
# 查看工作区与暂存区的差异(还没 add 的修改)
git diff

# 查看暂存区与最后一次 commit 的差异(即将提交的内容)
git diff --cached

# 查看某个文件的具体改动
git diff -- README.md

标签:git tag

标签(tag)用于给某个特定的 commit 打上标记,通常用于版本发布(如 v1.0v2.0.1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 给当前 commit 打标签
git tag v1.0.0

# 给指定 commit 打标签
git tag v1.0.0 <commit-hash>

# 给标签加注解
git tag -a v1.0.0 -m "Release version 1.0.0"

# 查看所有标签
git tag

# 查看某个标签对应的 commit 信息
git show v1.0.0

# 推送标签到远程
git push origin v1.0.0
git push origin --tags # 推送所有本地标签

# 删除标签
git tag -d v1.0.0
git push origin --delete v1.0.0 # 删除远程标签

Git 配置:本地与远程

配置层级

Git 的配置分为三个层级,优先级从高到低:

层级 命令 配置文件位置 生效范围
local git config(无选项) .git/config 当前仓库
global git config --global ~/.gitconfig 当前用户的所有仓库
system git config --system /etc/gitconfig 整台机器的所有用户

查找配置值时,Git 会从 local → global → system 逐层查找,最先找到的值胜出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查看所有配置及来源
git config --list --show-origin

# 查看单个配置
git config user.name

# 全局配置(首次使用 Git 必须设置)
git config --global user.name "Your Name"
git config --global user.email "your@email.com"

# 为某个项目单独覆盖
git config user.name "Project Only Name"

# 设置默认分支名
git config --global init.defaultBranch main

远程仓库管理

本地与远程仓库关系

一个本地仓库可以关联多个远程仓库。git clone 会自动添加一个名为 origin 的远程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看所有远程仓库
git remote -v

# 添加远程仓库
git remote add origin https://github.com/user/repo.git

# 删除远程仓库
git remote remove origin

# 重命名远程仓库
git remote rename origin upstream

# 查看远程仓库的详细信息
git remote show origin

远程交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 从远程拉取但不合并(fetch = 下载到本地,不自动合并)
git fetch origin

# 从远程拉取并合并(pull = fetch + merge)
git pull origin main

# 推送本地分支到远程
git push origin main

# 设置本地分支跟踪远程分支(push 时只需 git push)
git push -u origin main

# 删除远程分支
git push origin --delete old-branch

fetch vs pull 的区别: git fetch 只把远程的数据下载到本地数据库,不改变你当前的工作目录。你需要手动 git merge 来合并。git pull 是 fetch 完直接自动合并——方便但可能引入意外的合并提交。


三、项目规范:PR 与 Review 流程

一个人开发怎么折腾都行,但团队协作必须有一套统一规范。

3.1 分支规范

推荐的分支模型(简化版 Git Flow):

分支 用途
main 生产就绪,只合不入
dev 日常开发集成
feat/xxx 功能分支,从 dev 切出
fix/xxx 修复分支
release/x.x 发布前准备

原则: main 只接受 PR,不允许直接 push。每个 PR 必须经过 review。

要理解分支,先要知道 Git 是怎么存数据的。

Git 怎么存数据:snapshot 而非差异

很多版本控制系统(如 SVN)存储的是文件差异——每个版本只记录”这一行改了、那一行删了”。Git 的做法完全不同:每次提交都存储一个完整的目录快照

当你 git commit 时,Git 内部会创建三层对象:

  • Blob — 保存每个文件的内容(相当于文件内容的快照)
  • Tree — 保存目录结构,记录每个文件名对应哪个 blob(或子 tree)
  • Commit — 指向一棵 tree,还记录了作者、时间、提交信息、父 commit

假如你在一个有三个文件的目录中执行 git addgit commit,仓库里会形成这样的结构:

一次提交的内部结构:Commit → Tree → Blob

每个 commit 都通过 tree 记录了那一刻的完整目录状态,commit 之间通过 parent 指针串联成历史。

分支是什么

分支本质上就是一个指向 commit 的可移动指针。

Git 的默认分支名是 master(现在很多项目改叫 main)。每次提交,当前分支的指针就自动向前移动:

分支及其提交历史

创建分支

创建新分支就是创建一个新的指针,指向你当前所在的 commit:

1
git branch testing

执行后两个分支指向同一个 commit:

两个分支指向同一系列提交

那 Git 怎么知道当前在哪个分支上?它用一个特殊指针 HEAD 来标记。

HEAD:当前在哪

HEAD 指向当前分支

git branch 只创建分支,不会自动切换。要切换分支需要:

1
git checkout testing

这会移动 HEAD 让它指向 testing 分支:

切换分支后 HEAD 的指向变化

在分支上提交

现在在 testing 上做一次提交:

1
2
vim test.rb
git commit -a -m 'Make a change'

提交后 testing 指针向前移动,而 master 还停留在原地:

HEAD 所在的分支随提交前进

切回 master 看看:

1
git checkout master

切换分支时 Git 会自动还原工作目录里的文件,让它们回到 master 指向的那个 snapshot。这意味着你在 testing 上的工作会被”收起来”,master 目录干干净净。

切换分支后 HEAD 指向 master

然后在 master 上也做一次提交。现在两个分支从同一个起点分道扬镳——这就是分叉

分叉历史

git log --oneline --decorate --graph --all 可以清晰地看到分叉:

1
2
3
4
5
6
* c2b9e (HEAD, master) Make other changes
| * 87ab2 (testing) Make a change
|/
* f30ab Add feature #32
* 34ac2 Fix bug #1328
* 98ca9 Initial commit

分支在 Git 里只是一个 41 字节的文件(40 位 SHA-1 + 换行符),创建和销毁几乎零成本。这跟 SVN 等老式 VCS 完全不同——那些系统需要复制整个项目目录才能创建分支。

合并分支

场景一:快进合并(Fast-Forward)

假设在 master 上遇到了一个紧急 bug,需要立即修复。先创建一个 hotfix 分支:

1
2
3
git checkout -b hotfix
vim index.html
git commit -a -m 'Fix broken email address'

修复完成后切回 master 合并:

1
2
git checkout master
git merge hotfix

因为 hotfix 的 commit 直接排在 master 的后面(线性关系),Git 不需要做实际合并,只需要把 master 指针向前移动到 hotfix 的位置——这叫做快进合并

快进合并:直接把指针向前移

修复上线后,hotfix 分支没用了,删掉:

1
git branch -d hotfix
场景二:三方合并(Three-Way Merge)

回到正常开发。假设在 iss53 分支上做了几次提交,master 上也有新的提交,历史已经分叉。这时把 iss53 合并回 master:

1
2
git checkout master
git merge iss53

因为两个分支的顶端不能直接线性到达对方,Git 会做一次三方合并——取三个快照:两个分支的最新提交 + 它们的共同祖先:

三方合并所用的三个快照

合并的结果是一个合并提交,它有两个父 commit:

合并提交:双亲节点

合并完成后,iss53 上的工作已经全部包含在 master 里,可以删掉了:

1
git branch -d iss53

git branch -d 只会在分支已合并的前提下删除。如果分支还有未合并的工作,Git 会拒绝删除并提示你用 -D 强行删除(注意这会丢失提交)。

合并冲突

如果两个分支改了同一个文件的同一部分,Git 无法自动合并,会停下来等你解决:

1
2
3
4
5
6
7
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

手动编辑成想要的版本后,git add 标记为已解决,再 git commit 完成合并。

Rebase:另一种整合分支的方式

除了 git merge,Git 还提供了另一条整合分支的路径——rebase(变基)。

假设历史已经分叉:

简单的分叉历史

merge 是做一个三方合并,生成一个合并提交:

Merge:三方合并产生合并提交

rebase 的做法是:把你分支上的每一次变更”摘下来”,按顺序在目标分支的顶端重新应用:

1
2
git checkout experiment
git rebase master

Rebase:把 C4 的改动在 C3 上重新应用

rebase 的原理是:找到两个分支的共同祖先,把当前分支相对于共同祖先的每一个变更保存成临时文件,然后把当前分支重置到目标分支的最新 commit,再逐一应用这些变更。

rebase 完成后,experiment 分支的基底变成了 master 的最新提交,历史变成了一条直线。此时再切回 master 做合并,就变成一个简单的快进合并了:

1
2
git checkout master
git merge experiment

Rebase 后再 merge = 快进

merge vs rebase 怎么选

两种方式最终得到的目录快照是一样的,区别在于历史记录

  • merge 保留真实的分叉历史,能看到”什么时间并行开发了什么”
  • rebase 让历史呈线性,看起来像串行开发的,更加整洁

一个常用的策略是:用 rebase 整理本地尚未推送的 commit,用 merge 合并已推送的分支。具体来说:

  • 在推送到远程之前,用 git rebase -i 整理自己的 commit(合并、重排、改消息)
  • 合入团队共享分支时用 git merge,保留协作痕迹
黄金法则:永不 rebase 已推送的 commit

不要 rebase 已经推送到远程仓库、且其他人可能基于它工作的 commit。

rebase 的本质是丢弃旧的 commit,创建内容相同但哈希值不同的新 commit。如果别人已经基于旧 commit 做了开发,你突然把旧 commit 换掉,对方再次拉取时就会陷入混乱。

1
2
3
4
5
# ✅ 可以:rebase 自己本地还没 push 的 commit
git rebase -i HEAD~3

# ❌ 不要:rebase 已经 push 到团队分支的 commit
# (除非你100%确定只有你自己在用这个分支)

分支管理

1
2
3
4
5
git branch          # 列出所有分支(* 表示当前分支)
git branch -v # 查看每个分支的最新 commit
git branch --merged # 列出已合并到当前分支的分支(可以安全删除)
git branch -d <b> # 删除已合并的分支
git branch -D <b> # 强制删除未合并的分支

远程分支

当你的项目托管在 GitHub/GitLab 上时,本地仓库和远程仓库之间的分支关系通过远程跟踪分支来维护。每次 git fetchgit pull 都会更新这些指针。推送本地分支到远程:

1
git push origin <branch>

删除远程分支:

1
git push origin --delete <branch>

3.2 Commit Message 规范

推荐 Conventional Commits 格式:

1
2
3
<type>(<scope>): <description>

[optional body]
  • feat — 新功能
  • fix — 修复
  • refactor — 重构(不修 bug 不加功能)
  • docs — 文档
  • style — 样式/格式(非 CSS)
  • perf — 性能优化
  • test — 测试
  • chore — 构建/工具

示例:

1
2
3
4
5
feat(login): add OAuth2 Google login

- Add Google OAuth2 strategy
- Update User model with google_id field
- Add tests for auth flow

几条硬性规定:

  1. 标题不超过 72 字符
  2. 用祈使句(”Add” 不是 “Added” 或 “Adds”)
  3. 关联 Issue:Closes #123Refs #456

3.3 PR 规范

标题: 跟 commit message 标题一样,简短明了。

描述模板:

1
2
3
4
5
6
7
8
9
10
## Summary
<!-- 改了什么、为什么改 -->

## Test Plan
- [ ] 单元测试通过
- [ ] 本地手动测试
- [ ] 不影响现有功能

## Screenshots
<!-- 如果是 UI 改动,贴对比图 -->

PR 大小控制: 一个 PR 不要超过 400 行改动。超过的拆分成多个 PR。400 行是 review 的注意力极限。

3.4 Review 流程

提 PR 的一方:

  1. 自己先过一遍 diff,不要提交了就甩手
  2. PR 描述写清楚改动动机和测试方式
  3. 带上截图或录屏(UI 改动)
  4. 标注需要重点 Review 的文件

Review 的一方:

  1. 先理解上下文,再看具体代码
  2. 区分”必须改”和”建议改”
    • 必须改: 逻辑错误、安全问题、性能隐患
    • 建议改: 命名、风格、可读性提升(用 nit: 前缀标注)
  3. 对事不对人,用疑问句代替命令句
    • ❌ “这里写错了”
    • ✅ “这里是不是应该处理 null 的情况?”
  4. 如果 PR 太大,要求拆分而不是硬看

合入条件: 至少 1 人 Approve + CI 通过。


四、Git 进阶技巧

掌握了基础命令和协作规范之后,下面这些技巧能让你在实际场景中更游刃有余。

4.1 精细化暂存:git add -p

一个文件改了多处,但想分成多次 commit?git add -p 会把每个改动块逐一展示,让你决定是否暂存:

1
git add -p

每个 hunk 可以选:

  • y — 暂存
  • n — 跳过
  • s — 拆分成更小的 hunk
  • e — 手动编辑(精准选择要暂存的行)

4.2 临时保存现场:git stash

写到一半被叫去修紧急 bug?git stash 可以把当前工作区完整保存,回来再恢复:

1
2
3
4
5
6
7
8
9
10
11
# 暂存并加备注
git stash push -m "wip: refactoring auth module"

# 暂存 untracked 文件
git stash push -u

# 查看暂存列表
git stash list

# 把 stash 内容恢复到新分支
git stash branch new-feature stash@{0}

git clean — 清理 untracked 文件

git stash 配套的另一个命令。当你想要一个完全干净的工作目录时:

1
2
3
4
5
6
7
8
# 列出会被删除的文件(先预览,安全操作)
git clean -n -d

# 删除 untracked 文件和目录
git clean -f -d

# 连带删除 .gitignore 中忽略的文件
git clean -f -d -x

场景: 构建项目生成了大量临时文件,git clean -f -d 一键清理,回到干净状态。

4.3 查看历史与排查

git log 的进阶用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 图形化查看所有分支的历史
git log --oneline --graph --all

# 只看某个人的提交
git log --author="Math_Still"

# 只看改过某个文件的 commit
git log -- path/to/file

# 搜索 commit message
git log --grep="fix"

# 搜索代码改动(哪个 commit 改了这个关键词)
git log -S "functionName" -- path/to/file

# 查看每个 commit 的改动统计
git log --stat

git grep — 在 commit 历史中搜索代码

git log -S 更直接的搜索方式。直接在工作区或任意 commit 中搜索代码:

1
2
3
4
5
6
7
8
9
10
11
# 在工作区搜索
git grep "functionName"

# 在某个 commit 中搜索
git grep "functionName" v1.0.0

# 搜索并显示行号
git grep -n "functionName"

# 只看文件名
git grep --name-only "functionName"

git diff

查看工作区与暂存区的差别、或者两个分支之间的差异:

1
2
3
4
5
6
7
8
# 工作区 vs 暂存区
git diff

# 暂存区 vs 最后一次提交
git diff --cached

# 两个分支的差异
git diff main..feature

git blame — 找上下文

不是用来甩锅的,是用来找上下文的:

1
2
3
4
5
6
7
8
# 看每行谁改的、什么时候改的
git blame file.txt

# 只看某几行
git blame -L 10,30 file.txt

# 忽略空白变更
git blame -w file.txt

git bisect — 二分查找 bug

知道功能之前是好的、现在坏了,但不知道哪个 commit 引入的 bug:

1
2
3
4
5
6
7
8
9
git bisect start
git bisect bad # 标记当前版本有 bug
git bisect good v1.0 # 标记旧版本没 bug

# Git 会二分法 checkout 中间版本,你测试后标记
git bisect good # 或 git bisect bad

# 重复几次后 Git 告诉你第一个坏 commit
git bisect reset

也可以自动化:

1
2
git bisect start HEAD v1.0
git bisect run npm test # 自动用测试结果判断 good/bad

4.4 改写历史

git rebase -i — 交互式变基

整理 commit 历史的利器。可以把多个小 commit 合并成一个,或者修改 commit message:

1
git rebase -i HEAD~3  # 修改最近 3 个 commit
命令 作用
pick 保留
reword 只改 commit message
squash 合并到上一个 commit
fixup 合并但丢弃 message
drop 删除整个 commit

黄金法则: 只 rebase 尚未推送 的 commit。

git revert vs git reset

修改已提交的历史用这两个命令,但使用场景完全不同

命令 效果 适用场景
git reset --soft HEAD~1 撤销 commit,改动保留在暂存区 本地整理 commit
git reset --hard HEAD~1 彻底删除 commit 和改动 本地废弃的提交
git revert HEAD 创建一个新 commit 抵消目标 commit 撤销已推送的 commit

原则: 已推送的用 revert,未推送的用 reset

4.5 后悔与挽救

git reflog — 本地操作日志

git reflog 记录了所有 Git 操作的历史,包括被删除的 commit。如果你 git reset --hard 删过头了,可以从 reflog 找回:

1
git reflog

输出:

1
2
3
abc1234 HEAD@{0}: commit: fix login bug
def5678 HEAD@{1}: reset: moving to HEAD~1
ghi9012 HEAD@{2}: commit: add feature

找回:

1
git reset --hard ghi9012

注意: reflog 是本地的,不会同步到远程,默认 90 天后过期。

git cherry-pick — 精准取 commit

从其他分支挑一个 commit 应用到当前分支:

1
2
3
git cherry-pick <commit-hash>
git cherry-pick <hash1> <hash2>
git cherry-pick <hash1>^..<hash3> # 一段连续的 commit

场景: 在 dev 分支修复了一个 bug,hotfix 分支也需要同样的修复。不用合并整个分支,cherry-pick 那一个 commit 就行。

4.6 多分支并行:git worktree

不用反复 git stash + git switch,可以在不同目录同时维护多个分支:

1
2
3
4
5
6
7
8
# 在 ../hotfix 目录 checkout 一个 hotfix 分支
git worktree add ../hotfix fix/login-bug

# 列出所有 worktree
git worktree list

# 用完删除
git worktree remove ../hotfix

场景: 当前分支开发到一半,需要切到其他分支改个紧急问题。不用 stash,直接 git worktree add,在另一个目录改完 push 回来继续。


五、Git LFS:大文件管理

为什么要用 Git LFS

Git 擅长管理文本文件(代码),但不擅长管理大文件(图片、视频、模型文件、编译产物)。直接把大文件提交到 Git 仓库会导致:

  • 仓库体积暴涨,git clone 越来越慢
  • 每次修改大文件,Git 都要存一份完整副本(不是差异)
  • .git 目录越来越大

Git LFS(Large File Storage) 的解决思路:把大文件的内容替换成一个指针文件,真正的文件内容存储在远程 LFS 服务器上。

1
2
3
4
Git 仓库中的文件       LFS 服务器上的文件
───────────────── ─────────────────
pointer.txt → cat.jpg (50MB)
(只有 100 字节)

克隆仓库时只下载指针文件,真正的大文件按需下载。

安装与配置

1
2
3
4
5
6
7
# 安装 LFS(需要先装 Git)
brew install git-lfs # macOS
# apt install git-lfs # Ubuntu
# yum install git-lfs # CentOS

# 初始化 LFS(每个用户只需做一次)
git lfs install

使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 在仓库中指定哪些文件类型由 LFS 管理
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "*.mp4"
git lfs track "models/*.h5"

# 这会生成一个 .gitattributes 文件,提交它
git add .gitattributes
git commit -m "chore: configure Git LFS"

# 之后这些文件的使用方式和普通 Git 完全一样
git add design.psd
git commit -m "feat: add homepage design"
git push origin main # LFS 文件自动上传到 LFS 服务器

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看当前 LFS 跟踪规则
git lfs track

# 查看 LFS 管理的文件列表
git lfs ls-files

# 拉取 LFS 文件(clone 时自动执行,遗漏时可手动补)
git lfs pull

# 查看 LFS 使用统计
git lfs status

# 从仓库中移除某个文件的 LFS 跟踪
git lfs untrack "*.psd"
git add .gitattributes
git commit -m "chore: stop tracking psd with LFS"

注意事项

  • LFS 有存储配额限制(GitHub 免费套餐 1GB 存储 + 每月 1GB 流量)
  • 已提交的大文件不会自动变小 — 需要用 git lfs migrate 重写历史
  • 服务器端也需要支持 LFS(GitHub、GitLab、Gitee 都支持)
  • 团队成员都要安装 git lfs install,否则 clone 下来的是指针文件而不是真实内容
🐱
觉得这篇文章很棒!