第17章:Pull Request 进阶 —— 成为开源贡献者

第17章:Pull Request 进阶 —— 成为开源贡献者

Pull Request(PR),有人叫它"拉取请求",有人叫它"合并请求"(MR)。不管叫什么,它都是现代软件开发中最重要的协作工具之一。这一章,让我们从"会发 PR"进化到"精通 PR"!


17.1 PR 不是终点,是沟通的开始

很多新手以为,创建 PR 就是开发工作的终点——“我代码写完了,PR 一发,任务完成!”

大错特错!

PR 不是终点,而是沟通的开始。它是你向团队说"嘿,我写了一些代码,大家来看看"的方式。

PR 的真正作用

graph LR
    A[编写代码] --> B[创建 PR]
    B --> C[代码审查]
    C --> D[讨论修改]
    D --> E{审查通过?}
    E -->|否| F[修改代码]
    F --> C
    E -->|是| G[合并代码]
    G --> H[部署上线]

PR 是沟通工具

好的 PR 描述

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
## 功能描述
实现了用户登录功能,支持邮箱和密码登录。

## 改动内容
- 添加了 LoginForm 组件
- 实现了 /api/login 接口对接
- 添加了表单验证

## 测试方法
1. 访问 /login 页面
2. 输入邮箱和密码
3. 点击登录按钮
4. 验证是否跳转到首页

## 截图
[登录页面截图]

## 关联 Issue
Closes #123

差的 PR 描述

1
实现登录功能

PR 的生命周期

flowchart TD
    A[Draft PR<br/>草稿状态] --> B[Ready for Review<br/>准备审查]
    B --> C[Review 中<br/>收集反馈]
    C --> D[修改代码<br/>处理意见]
    D --> E[再次 Review]
    E --> F{通过?}
    F -->|否| C
    F -->|是| G[Approved<br/>审查通过]
    G --> H[Merged<br/>已合并]
    H --> I[Closed<br/>关闭]

PR 中的沟通技巧

作为 PR 作者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
## ✅ 要做的

- [ ] 写清楚的 PR 描述
- [ ] 说明改动的原因
- [ ] 提供测试方法
- [ ] 及时响应 review 意见
- [ ] 感谢 reviewer 的时间

## ❌ 不要做的

- [ ] 提交巨大的 PR(>500 行)
- [ ] 不说明改动原因
- [ ] 对 review 意见不耐烦
- [ ] 不测试就提交

作为 Reviewer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
## ✅ 要做的

- [ ] 及时 review(24 小时内)
- [ ] 提出建设性意见
- [ ] 解释为什么需要修改
- [ ] 赞美好的代码

## ❌ 不要做的

- [ ] 只评论 "LGTM"(Looks Good To Me)
- [ ] 用命令语气
- [ ] 拖延 review
- [ ] 过于苛刻

PR 评论的礼仪

好的评论

这个实现很巧妙!不过我在想,如果用户输入特殊字符,
会不会有问题?建议加个输入清理。

差的评论

这里有问题,改一下。

PR 是学习的场所

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
## Review 对话示例

**Reviewer**: 这里为什么要用递归而不是迭代?

**Author**: 因为树的深度不确定,递归更直观。

**Reviewer**: 但是如果树很深,可能会导致栈溢出。
考虑用队列实现迭代版本?

**Author**: 好主意!我改一下。

---

**结果**:双方都学到了东西,代码质量提高了。

PR 的社交属性

PR 不只是代码审查,还是:

  • 知识分享:展示你的解决方案
  • 团队建设:通过代码了解同事
  • 文档留存:PR 历史可以追溯决策过程

小贴士

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
## PR 模板(保存为 .github/pull_request_template.md)

## 改动描述
<!-- 描述这个 PR 做了什么 -->

## 改动原因
<!-- 为什么要做这个改动 -->

## 测试方法
<!-- 如何验证这个改动 -->

## 截图/GIF
<!-- 如果有 UI 改动 -->

## 关联 Issue
<!-- 关联的 Issue 编号 -->

## 检查清单
- [ ] 代码通过测试
- [ ] 没有引入新的 lint 错误
- [ ] 文档已更新(如果需要)

记住:PR 是沟通的开始,不是开发的终点。好好写 PR,让协作更顺畅!


17.2 小步提交:PR 控制在 300 行以内

想象一下:你打开一个 PR,发现它修改了 50 个文件,新增了 3000 行代码。你的第一反应是什么?

“卧槽,这怎么看?”

巨大的 PR 是 reviewer 的噩梦,也是 bug 的温床。

为什么大 PR 不好?

graph TD
    A[大 PR] --> B[审查困难]
    A --> C[容易遗漏问题]
    A --> D[合并冲突多]
    A --> E[回滚成本高]
    
    B --> F[审查质量下降]
    C --> G[Bug 流入生产]
    D --> H[开发效率降低]
    E --> I[风险增加]

数据说话

  • PR 小于 200 行:审查时间平均 1 小时
  • PR 大于 500 行:审查时间平均 4 小时,且容易遗漏问题
  • PR 大于 1000 行:几乎不可能被彻底审查

小步提交的好处

  1. 审查更快:Reviewer 能在短时间内完成审查
  2. 反馈更快:问题能及时发现,及时修复
  3. 冲突更少:代码合并更频繁,冲突更少
  4. 回滚更容易:出问题只回滚一小部分

如何控制 PR 大小?

策略一:按功能拆分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# ❌ 不要这样做:一个 PR 包含多个功能
git checkout -b feature/big-feature
git commit -m "feat: 实现登录、注册、找回密码、用户管理"
# 修改了 20 个文件,1000+ 行代码

# ✅ 应该这样做:拆分成多个小 PR
# PR 1: 登录功能
git checkout -b feature/login
git commit -m "feat: 实现用户登录"
# 5 个文件,150 行代码

# PR 2: 注册功能
git checkout -b feature/register
git commit -m "feat: 实现用户注册"
# 5 个文件,180 行代码

# PR 3: 找回密码
git checkout -b feature/forgot-password
git commit -m "feat: 实现找回密码"
# 4 个文件,120 行代码

策略二:按层次拆分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 一个功能可以拆分成多个层次

# PR 1: 数据层(Model/Repository)
git checkout -b feature/user-model
git commit -m "feat: 添加用户数据模型"

# PR 2: 业务层(Service)
git checkout -b feature/user-service
git commit -m "feat: 添加用户业务逻辑"

# PR 3: 接口层(API)
git checkout -b feature/user-api
git commit -m "feat: 添加用户接口"

# PR 4: 前端层(UI)
git checkout -b feature/user-ui
git commit -m "feat: 添加用户界面"

策略三:按文件类型拆分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# PR 1: 只改后端代码
git commit -m "feat: 实现用户 API"

# PR 2: 只改前端代码
git commit -m "feat: 实现用户界面"

# PR 3: 只改测试代码
git commit -m "test: 添加用户模块测试"

# PR 4: 只改文档
git commit -m "docs: 更新用户模块文档"

PR 大小的黄金法则

1
2
3
4
5
6
7
## PR 大小检查清单

- [ ] 修改文件数 < 10 个
- [ ] 新增/删除代码行数 < 300 行
- [ ] 只做一件事(Single Responsibility)
- [ ] 可以在 30 分钟内审查完
- [ ] 标题能概括所有改动

特殊情况:大改动怎么办?

有时候确实需要大改动,比如重构。这时候怎么办?

方案一:分阶段 PR

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
## 重构计划(分 5 个 PR)

### PR 1: 准备工作
- 添加新的工具函数
- 不影响现有代码

### PR 2: 迁移模块 A
- 只改模块 A
- 其他模块保持不变

### PR 3: 迁移模块 B
- 只改模块 B

### PR 4: 迁移模块 C
- 只改模块 C

### PR 5: 清理旧代码
- 删除已迁移的旧代码
- 更新文档

方案二:使用 Feature Flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 使用 feature flag 控制新功能
const useNewFeature = process.env.ENABLE_NEW_FEATURE === 'true';

function userLogin(credentials) {
  if (useNewFeature) {
    return newLoginImplementation(credentials);
  } else {
    return oldLoginImplementation(credentials);
  }
}

这样可以把新功能逐步合并到主分支,而不影响现有功能。

如何拆分已有的巨大 PR?

如果你已经写了一个大 PR,怎么拆分?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 1. 创建备份分支
git checkout -b backup/big-pr

# 2. 回到原分支
git checkout feature/big-pr

# 3. 交互式 rebase,拆分提交
git rebase -i main

# 4. 在交互式编辑器中,把大的提交拆分成多个小提交
# 把 "pick" 改成 "edit",然后拆分

# 5. 或者手动拆分
git reset HEAD~5  # 回到 5 个提交前
git add file1.js file2.js
git commit -m "feat: 功能 A"
git add file3.js
git commit -m "feat: 功能 B"
# ...

PR 大小的统计工具

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 查看 PR 的统计信息
git diff --stat main...feature-branch

# 输出示例:
#  src/components/Login.jsx    |  50 ++++++++
#  src/components/Register.jsx |  60 ++++++++
#  src/api/auth.js             |  80 +++++++++++
#  src/utils/validation.js     |  30 +++++
#  tests/auth.test.js          | 100 +++++++++++++
#  5 files changed, 320 insertions(+)

# 如果超过 300 行,考虑拆分

小步提交的工作流

flowchart TD
    A[开始开发] --> B{功能复杂?}
    B -->|是| C[拆分成子任务]
    B -->|否| D[直接开发]
    C --> E[逐个完成子任务]
    E --> F[每个子任务一个 PR]
    D --> G[完成开发]
    F --> H[并行审查]
    G --> I[创建 PR]
    H --> J{都通过?}
    I --> K[审查]
    J -->|否| E
    K --> L{通过?}
    L -->|否| D
    L -->|是| M[合并]
    J -->|是| M

团队约定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
## PR 大小团队约定

### 硬性限制
- 单个 PR 不超过 400 行代码
- 单个 PR 不超过 15 个文件
- 单个 PR 只做一件事

### 软性建议
- 理想大小:100-200 行
- 审查时间:不超过 30 分钟
- 响应时间:24 小时内

### 例外情况
- 配置文件更新(如 package.json)
- 自动生成的代码
- 文档更新
- 需要提前说明

小贴士

1
2
3
4
5
6
7
# 配置 Git 别名,快速查看 PR 大小
git config --global alias.pr-size 'diff --stat main...HEAD'

# 使用
git pr-size

# 输出修改统计,判断是否过大

记住:小步快跑,胜过大步摔跤。控制 PR 大小,让审查更高效!


17.3 关联 Issue:Closes #123 的魔法

你有没有遇到过这种情况:PR 合并了,但对应的 Issue 还开着,需要手动去关闭?

Closes #123 就是来解决这个问题的魔法咒语!

什么是 Issue 关联?

Issue 关联是指在 PR 描述中引用相关的 Issue,让 GitHub/GitLab 自动建立关联,并在 PR 合并时自动关闭 Issue。

魔法关键词

GitHub/GitLab 支持以下关键词来自动关闭 Issue:

关键词示例作用
CloseClose #123关闭 Issue
ClosesCloses #123关闭 Issue
ClosedClosed #123关闭 Issue
FixFix #123修复并关闭 Issue
FixesFixes #123修复并关闭 Issue
FixedFixed #123修复并关闭 Issue
ResolveResolve #123解决并关闭 Issue
ResolvesResolves #123解决并关闭 Issue
ResolvedResolved #123解决并关闭 Issue

基础用法

1
2
3
4
5
## PR 描述示例

实现了用户登录功能。

Closes #123

当这个 PR 被合并后,Issue #123 会自动关闭。

高级用法

关闭多个 Issue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
## PR 描述示例

实现了用户认证系统。

Closes #123
Closes #124
Closes #125

# 或者简写
Closes #123, closes #124, closes #125

关闭其他仓库的 Issue

1
2
3
4
5
## PR 描述示例

修复了跨仓库依赖的问题。

Closes organization/other-repo#456

部分关闭(不自动关闭)

1
2
3
4
5
6
7
8
9
## PR 描述示例

实现了登录功能的第一部分。

Related to #123
Refs #123
See #123

# 这些不会自动关闭 Issue,只是建立关联

在提交信息中使用

除了在 PR 描述中使用,也可以在提交信息中使用:

1
2
3
4
5
6
7
# 提交时关联 Issue
git commit -m "feat: 实现用户登录

Closes #123"

# 或者多行提交
git commit -m "feat: 实现用户登录" -m "Closes #123"

Issue 关联的工作流

flowchart TD
    A["创建 Issue #123<br/> 实现登录功能"] --> B[开始开发]
    B --> C[创建 PR]
    C --> D["PR 描述写 Closes #123"]
    D --> E[代码审查]
    E --> F{审查通过?}
    F -->|否| G[修改代码]
    G --> E
    F -->|是| H[合并 PR]
    H --> I[Issue #123<br/>自动关闭]
    I --> J[通知相关人员]

完整的 PR 描述模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
## 功能描述
实现了用户登录功能,支持邮箱和密码登录。

## 改动内容
- 添加了 LoginForm 组件
- 实现了 /api/login 接口对接
- 添加了表单验证

## 测试方法
1. 访问 /login 页面
2. 输入测试账号:test@example.com / password
3. 点击登录按钮
4. 验证是否跳转到首页

## 截图
![登录页面](screenshot.png)

## 关联 Issue
Closes #123

## 检查清单
- [x] 代码通过测试
- [x] 没有引入新的 lint 错误
- [x] 文档已更新

在 GitHub 上的效果

当你在 PR 中写了 Closes #123

  1. PR 页面会显示 “Linked issues” 区域
  2. Issue #123 页面会显示 “Linked pull requests”
  3. PR 合并后,Issue 自动关闭
  4. 关闭的 Issue 会显示 “closed by #456”(PR 编号)

在 GitLab 上的效果

GitLab 类似,但有一些额外功能:

1
2
3
4
5
6
7
8
9
## MR 描述示例

实现了用户登录功能。

Closes #123

/label ~feature ~authentication
/milestone "Sprint 3"
/assign @username

GitLab 还支持 Quick Actions,可以在 MR 描述中执行命令。

关联但不关闭

有时候你想关联 Issue,但不想自动关闭(比如这个 PR 只是部分解决了 Issue):

1
2
3
4
5
6
7
8
## PR 描述示例

实现了登录功能的前端部分。

Related to #123
Progress on #123

# 这些不会关闭 Issue,只是建立关联

最佳实践

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
## Issue 关联最佳实践

### ✅ 要做的
- [ ] 每个 PR 都关联相关的 Issue
- [ ] 使用 Closes/Fixes/Resolves 自动关闭已完成的 Issue
- [ ] 如果 PR 只是部分解决,使用 Related to/Refs
- [ ] 在提交信息中也使用关键词(可选)

### ❌ 不要做的
- [ ] 忘记关联 Issue
- [ ] 使用错误的关键词(比如用 Close 但实际上只是相关)
- [ ] 一个 PR 关闭太多 Issue(超过 5 个)
- [ ] 在无关的 PR 中引用 Issue

自动化工具

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 使用 commitlint 检查提交信息格式
# 配置 commitlint.config.js

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'references-empty': [2, 'never'],  // 要求必须有 Issue 引用
  },
  parserPreset: {
    parserOpts: {
      issuePrefixes: ['#', 'JIRA-'],  // 支持的 Issue 前缀
    },
  },
};

小贴士

1
2
3
4
5
# 查看提交是否关联了 Issue
git log --oneline --grep="#123"

# 查看所有关联了 Issue 的提交
git log --oneline --all --grep="#"

记住:Closes #123 不只是文字,它是连接 PR 和 Issue 的魔法桥梁!


17.4 Draft PR:先占坑,慢慢完善

想象一下:你正在开发一个大功能,需要几天时间。你想让同事知道你在做什么,避免重复工作,但代码还没写完,不想被审查。

这时候,Draft PR 就派上用场了!

什么是 Draft PR?

Draft PR(草稿 PR)是一种特殊的 PR 状态,表示"我还在开发中,不要审查,但可以先看看"。

创建 Draft PR

在 GitHub 上

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
## 方法1:创建 PR 时选择

1. 点击 "New Pull Request"
2. 选择分支后,点击下拉箭头
3. 选择 "Create draft pull request"

## 方法2:在 PR 标题前加 "[WIP]" 或 "[DRAFT]"

标题:"[WIP] 实现用户登录功能"

# WIP = Work In Progress(进行中)

在 GitLab 上

1
2
3
4
5
6
7
8
## 方法1:创建 MR 时选择

1. 点击 "New merge request"
2. 勾选 "Mark as draft"

## 方法2:在 MR 标题前加 "Draft:" 或 "WIP:"

标题:"Draft: 实现用户登录功能"

Draft PR 的特点

graph LR
    A[Draft PR] --> B[不能合并]
    A --> C[不会通知审查者]
    A --> D[显示为灰色]
    A --> E[可以持续推送]
    
    F[Ready PR] --> G[可以合并]
    F --> H[会通知审查者]
    F --> I[显示为绿色]

Draft PR 的使用场景

场景一:早期分享

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
## Draft PR 描述示例

🚧 Work In Progress 🚧

正在实现用户登录功能,目前完成了:
- [x] 登录表单 UI
- [ ] API 对接
- [ ] 表单验证
- [ ] 错误处理

欢迎大家提前看看,给点建议!

/cc @前端组长

场景二:避免重复工作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
## Draft PR 描述示例

🚧 进行中

正在重构用户模块,预计本周完成。

@小红 你之前说要做用户相关的功能,
可以先看看这个 PR,避免冲突。

相关 Issue: #123

场景三:寻求帮助

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
## Draft PR 描述示例

🚧 需要帮助

实现登录功能时遇到了一个问题:

`src/api/auth.js` 第 45 行,
我不知道怎么处理 JWT 过期的情况。

有人能帮忙看看吗?

@后端大神

Draft PR 的工作流

flowchart TD
    A[开始开发] --> B[创建 Draft PR]
    B --> C[持续推送代码]
    C --> D[收集早期反馈]
    D --> E{开发完成?}
    E -->|否| C
    E -->|是| F[标记为 Ready]
    F --> G[正式审查]
    G --> H{审查通过?}
    H -->|否| I[修改代码]
    I --> G
    H -->|是| J[合并]

标记为 Ready

当代码完成后,将 Draft PR 转为正式 PR:

GitHub

1
2
3
4
5
6
7
## 方法1:点击 "Ready for review"

在 PR 页面点击 "Ready for review" 按钮

## 方法2:修改标题

去掉 "[WIP]" 或 "[DRAFT]" 前缀

GitLab

1
2
3
4
5
6
7
## 方法1:点击 "Mark as ready"

在 MR 页面点击 "Mark as ready" 按钮

## 方法2:修改标题

去掉 "Draft:" 或 "WIP:" 前缀

Draft PR 的自动化

GitHub Actions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# .github/workflows/draft-check.yml
name: Draft PR Check

on:
  pull_request:
    types: [opened, edited]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - name: Check if PR is draft
        run: |
          if [ "${{ github.event.pull_request.draft }}" == "true" ]; then
            echo "This is a draft PR, skipping CI"
            exit 0
          fi

自动标记 Draft

1
2
3
4
5
# 使用 GitHub CLI 创建 Draft PR
gh pr create --draft --title "WIP: 新功能" --body "开发中..."

# 标记为 Ready
gh pr ready

Draft PR 的最佳实践

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
## Draft PR 检查清单

### 创建 Draft PR 时
- [ ] 在描述中说明当前进度
- [ ] 列出待办事项(TODO)
- [ ] 说明预计完成时间
- [ ] @ 相关的人(如果需要)

### Draft PR 期间
- [ ] 持续推送代码
- [ ] 更新进度
- [ ] 回应早期反馈
- [ ] 完成后标记为 Ready

### 标记为 Ready 时
- [ ] 确保代码通过测试
- [ ] 更新 PR 描述
- [ ] 添加审查者
- [ ] 移除 TODO 列表

Draft PR vs 普通分支

特性Draft PR普通分支
可见性所有人可见只有知道链接的人可见
CI/CD可选运行通常不运行
讨论可以评论不能评论
进度追踪方便不方便
透明度

小贴士

1
2
3
4
# 使用 GitHub CLI 快速创建 Draft PR
gh pr create --draft --fill

# --fill 会自动使用提交信息填充标题和描述

记住:Draft PR 是"先占坑"的艺术——让大家知道你在做什么,同时给自己完善的时间!


17.5 处理 Review 意见:虚心接受,及时修改

PR 发出去,Review 意见随之而来。这时候,你的态度决定了协作的效率和团队的氛围。

Review 意见的几种类型

graph TD
    A[Review 意见] --> B[必须修改<br/>Blocking]
    A --> C[建议修改<br/>Suggestion]
    A --> D[问题询问<br/>Question]
    A --> E[表扬赞赏<br/>Praise]
    
    B --> F[必须解决才能合并]
    C --> G[可选,但建议采纳]
    D --> H[需要回复解释]
    E --> I[感谢 reviewer]

如何回复 Review 意见

类型一:必须修改(Blocking)

Reviewer 评论

这里有一个安全漏洞,用户输入没有验证,
可能导致 XSS 攻击。必须修复。

你的回复

感谢指出!确实是个严重问题。

已修复:在 `src/utils/validation.js` 中添加了输入清理函数,
所有用户输入都经过 HTML 转义。

修改提交:abc1234

请再次 review。

类型二:建议修改(Suggestion)

Reviewer 评论

建议这里使用解构赋值,代码会更简洁:

const { name, email } = user;

而不是:
const name = user.name;
const email = user.email;

你的回复(同意)

好建议!确实更简洁。

已修改,请查看。

你的回复(不同意)

感谢建议!

不过这里我故意没有使用解构,因为后面还需要用到 user 的其他属性,
解构后反而需要重新组装。保持原样可能更清晰。

你觉得呢?

类型三:问题询问(Question)

Reviewer 评论

这里为什么要用递归而不是迭代?

你的回复

好问题!

因为树的深度不确定,递归实现更直观。
不过你说得对,如果树太深可能导致栈溢出。

我改成了迭代版本,使用队列实现。
请查看新的实现。

类型四:表扬赞赏(Praise)

Reviewer 评论

这个错误处理写得很好,考虑得很全面!👍

你的回复

谢谢!是从之前的项目踩过的坑里学到的 😄

修改代码的流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1. 查看 Review 意见
# 在 GitHub/GitLab PR 页面查看

# 2. 切换到 PR 分支
git checkout feature/my-feature

# 3. 修改代码
# 根据 Review 意见修改

# 4. 提交修改
git add .
git commit -m "fix: 根据 review 意见修改

- 修复 XSS 漏洞
- 添加输入验证
- 优化错误处理

Refs: PR #456"

# 5. 推送
git push origin feature/my-feature

# 6. 在 PR 页面回复 Review 意见
# 说明修改了哪些地方

处理 Review 的心态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
## ✅ 正确的心态

- [ ] Review 是帮助,不是批评
- [ ] 每个人都有盲点,别人能看到你看不到的问题
- [ ] 感谢 reviewer 花时间帮你检查
- [ ] 有不同意见可以讨论,但不要情绪化
- [ ] 及时响应,不要让 reviewer 等太久

## ❌ 错误的心态

- [ ] "Reviewer 在挑刺"
- [ ] "我的代码没问题,是你不懂"
- [ ] "这点小问题也要改?"
- [ ] 不回复,直接修改
- [ ] 拖延不处理

常见错误回复

错误示例 1:不解释直接改

(没有回复,直接修改)

# Reviewer 不知道你是否理解了问题

错误示例 2:情绪化回复

你不懂,这里必须这样写!

# 即使你是正确的,这种态度也不好

错误示例 3:拖延

(3 天后才回复)

# Reviewer 可能已经忘记上下文了

修改后的标记

在 GitHub/GitLab 上,修改后的代码会显示 “Outdated”:

 reviewer-name 3 days ago
 这里需要添加错误处理

 作者 2 days ago
 已添加,请查看

 [Outdated]  # 表示这段代码已经被修改

Reviewer 可以点击 “Resolve conversation” 标记为已解决。

不同意 Review 意见怎么办?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
## 如何礼貌地表达不同意见

### 1. 先感谢
"感谢你的建议!"

### 2. 解释你的考虑
"这里我考虑的是..."
"之所以这样写是因为..."

### 3. 提出你的方案
"我觉得可以改成..."
"或者我们可以..."

### 4. 征求对方意见
"你觉得呢?"
"这样是否可行?"

### 示例
"感谢建议!这里我故意没有提取成函数,
是因为这个逻辑只在当前组件使用,提取后反而增加跳转成本。
不过如果后面其他组件也需要,我会提取到 utils。
你觉得当前这样 OK 吗?"

Review 响应时间

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
## 响应时间约定

### 紧急修复
- Review 意见:2 小时内响应
- 修改完成:当天完成

### 普通功能
- Review 意见:24 小时内响应
- 修改完成:2 天内完成

### 大型重构
- Review 意见:24 小时内响应
- 修改完成:按约定时间

### 如果无法按时完成
- 在 PR 中说明原因
- 给出预计完成时间
- 保持沟通

自动化工具

1
2
3
4
5
# 使用 GitHub CLI 查看 PR review 状态
gh pr view --repo owner/repo

# 查看 review 意见
gh pr review --repo owner/repo --comments

小贴士

1
2
3
4
5
6
## Review 处理技巧

1. **批量处理**:如果有多个小建议,可以批量修改后统一回复
2. **分优先级**:先处理 blocking 问题,再处理 suggestions
3. **保持沟通**:如果不确定怎么改,先问清楚
4. **记录学习**:把常见的 review 意见记录下来,下次避免

记住:Review 是团队协作的润滑剂,虚心接受,及时修改,共同进步!


17.6 合并前的 Rebase:让历史更干净

你的 feature 分支上有一堆提交:“fix typo”、“fix again”、“fix fix”… 合并到 main 分支时,这些乱七八糟的提交历史会污染主分支。

这时候,Rebase 就是你的救星!

什么是 Rebase?

Rebase(变基)是将一个分支的提交"移动"到另一个分支的最新提交之后,同时可以整理提交历史。

graph LR
    A[main: A---B---C] --> D[feature: A---B---C---D---E]
    
    E --> F[Rebase 后]
    F --> G[main: A---B---C]
    G --> H[feature: A---B---C---D'---E']
    
    D' --> I[干净的线性历史]

为什么要 Rebase?

原因一:保持线性历史

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 不使用 Rebase:分叉历史
git log --oneline --graph
* abc1234 (HEAD -> main) Merge pull request #123
|\
| * def5678 (feature/login) 实现登录
| * 9ab9012 添加登录表单
* | 0123456 修复 bug
|/
* 7890abc 初始提交

# 使用 Rebase:线性历史
git log --oneline --graph
* def5678 (HEAD -> main) 实现登录
* 9ab9012 添加登录表单
* 0123456 修复 bug
* 7890abc 初始提交

原因二:整理提交

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Rebase 前:乱七八糟的提交
git log --oneline
abc1234 fix typo
def5678 fix again
9ab9012 fix fix
0123456 实现登录功能

# Rebase 后:整洁的提交
git log --oneline
def5678 feat: 实现登录功能
9ab9012 feat: 添加登录表单

合并前 Rebase 的步骤

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 1. 切换到 feature 分支
git checkout feature/my-feature

# 2. 获取远程最新代码
git fetch origin

# 3. Rebase 到最新的 main
git rebase origin/main

# 4. 如果有冲突,解决后继续
git add .
git rebase --continue

# 5. 推送(需要强制推送)
git push origin feature/my-feature --force-with-lease

交互式 Rebase

交互式 Rebase 可以编辑、合并、删除提交:

1
2
3
4
5
# 交互式 Rebase 最近 5 个提交
git rebase -i HEAD~5

# 或者 Rebase 到某个提交
git rebase -i abc1234

会打开编辑器,显示:

pick abc1234 feat: 实现登录功能
pick def5678 fix: 修复 typo
pick 9ab9012 fix: 再次修复
pick 0123456 fix: 修复修复

# Rebase abc1234..0123456 onto def5678
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# d, drop <commit> = remove commit

常用操作

合并提交(Squash)

# 修改前
pick abc1234 feat: 实现登录功能
pick def5678 fix: 修复 typo
pick 9ab9012 fix: 再次修复
pick 0123456 fix: 修复修复

# 修改后
pick abc1234 feat: 实现登录功能
fixup def5678 fix: 修复 typo
fixup 9ab9012 fix: 再次修复
fixup 0123456 fix: 修复修复

# 结果:4 个提交合并成 1 个

修改提交信息

# 修改前
pick abc1234 feat: 实现登录功能

# 修改后
reword abc1234 feat: 实现用户登录功能

# 会弹出编辑器让你修改提交信息

删除提交

# 修改前
pick abc1234 feat: 实现登录功能
pick def5678 fix: 修复 typo
pick 9ab9012 temp: 临时调试

# 修改后
pick abc1234 feat: 实现登录功能
pick def5678 fix: 修复 typo
# 删除 9ab9012 这一行

调整提交顺序

# 修改前
pick abc1234 feat: 实现登录功能
pick def5678 feat: 添加表单验证
pick 9ab9012 feat: 添加错误提示

# 修改后
pick abc1234 feat: 实现登录功能
pick 9ab9012 feat: 添加错误提示
pick def5678 feat: 添加表单验证

# 提交顺序改变了

Rebase 的工作流程

flowchart TD
    A[功能开发完成] --> B[git rebase -i HEAD~n]
    B --> C[整理提交历史]
    C --> D{有冲突?}
    D -->|是| E[解决冲突]
    E --> F[git rebase --continue]
    F --> D
    D -->|否| G[整理完成]
    G --> H[git push --force-with-lease]
    H --> I[创建 PR]
    I --> J[合并到 main]

Rebase vs Merge

特性RebaseMerge
历史线性分叉
可读性
冲突处理逐个提交处理一次性处理
安全性修改历史保留历史
适用场景feature 分支main 分支

Rebase 的黄金法则

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
## 🚨 Rebase 黄金法则

**永远不要对已经推送到远程的提交使用 Rebase!**

### 为什么?

Rebase 会修改提交历史,生成新的 commit hash。
如果其他人基于旧的提交工作,会造成混乱。

### 正确做法

- ✅ 在本地 feature 分支上 Rebase
- ✅ 推送前 Rebase
- ✅ 使用 --force-with-lease 推送
- ❌ 不要在 main/develop 分支上 Rebase
- ❌ 不要对已经合并的提交 Rebase

常见问题

问题1:Rebase 后冲突太多

解决

1
2
3
4
5
# 如果冲突太多,可以放弃 Rebase
git rebase --abort

# 改用普通的 merge
git merge origin/main

问题2:误删了提交

解决

1
2
3
4
5
# 从 reflog 恢复
git reflog

# 找到误删前的状态
git reset --hard abc1234

问题3:Rebase 后推送被拒绝

解决

1
2
3
4
# 使用 --force-with-lease
git push origin feature/my-feature --force-with-lease

# 不要用 --force!

小贴士

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 配置别名,方便 Rebase
git config --global alias.rb 'rebase'
git config --global alias.rbi 'rebase -i'
git config --global alias.rbc 'rebase --continue'
git config --global alias.rba 'rebase --abort'

# 使用
git rbi HEAD~5
git rbc
git rba

记住:Rebase 是整理历史的艺术,让提交历史像诗一样优美!


17.7 squash 合并:把多个提交变成一个

一个功能开发了 20 个提交,合并到 main 分支时,这 20 个提交都会出现在主分支历史中。

Squash 合并可以把这 20 个提交压缩成 1 个,让主分支历史更简洁。

什么是 Squash 合并?

Squash 合并是将一个分支的所有提交压缩成一个提交,然后合并到目标分支。

graph LR
    A[feature 分支] --> B[commit 1]
    A --> C[commit 2]
    A --> D[commit 3]
    A --> E[...]
    A --> F[commit 20]
    
    B --> G[Squash 后]
    C --> G
    D --> G
    E --> G
    F --> G
    
    G --> H[main 分支只有一个合并提交]

在 GitHub 上使用 Squash 合并

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
## 操作步骤

1. 打开 PR 页面
2. 点击 "Merge pull request" 按钮旁边的下拉箭头
3. 选择 "Squash and merge"
4. 编辑合并提交信息
5. 点击 "Squash and merge"

## 合并后的效果

feature 分支的 20 个提交变成 main 分支的 1 个提交

在 GitLab 上使用 Squash 合并

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
## 操作步骤

1. 打开 MR 页面
2. 勾选 "Squash commits"
3. 编辑提交信息
4. 点击 "Merge"

## 或者让作者 Squash

作者可以在本地执行:
git rebase -i HEAD~20
# 把 pick 改成 squash/fixup

Squash 合并的提交信息

Squash 合并时,可以编辑提交信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
## 默认提交信息

feat: 实现用户登录功能 (#123)

* feat: 添加登录表单
* feat: 实现登录 API
* feat: 添加错误处理
* fix: 修复表单验证
* fix: 修复登录状态保持
...

## 建议的提交信息

feat: 实现用户登录功能 (#123)

- 添加登录表单 UI
- 实现 /api/login 接口对接
- 添加表单验证和错误处理
- 保持登录状态

Closes #100

Squash 合并 vs 普通合并

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 普通合并(Merge commit)
git log --oneline --graph
*   abc1234 (HEAD -> main) Merge pull request #123
|\
| * def5678 (feature/login) fix: 修复登录状态
| * 9ab9012 fix: 修复表单验证
| * 0123456 feat: 添加错误处理
| * 7890abc feat: 实现登录 API
| * fedcba9 feat: 添加登录表单
* | 1111111 (origin/main) 其他提交
|/

# Squash 合并
git log --oneline --graph
* abc1234 (HEAD -> main) feat: 实现用户登录功能 (#123)
* 1111111 (origin/main) 其他提交

什么时候用 Squash 合并?

✅ 适合 Squash 的场景

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
1. 功能分支有很多小提交
   - "fix typo"
   - "fix again"
   - "debug"
   - "wip"

2. 提交历史比较乱
   - 多次合并 main 分支
   - 多次 revert

3. 不需要保留详细历史
   - 小功能
   - 实验性代码

❌ 不适合 Squash 的场景

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
1. 需要保留详细历史
   - 大型重构
   - 需要追溯每个改动

2. 多个独立功能
   - 一个分支做了多个不相关的功能
   - 应该拆分成多个 PR

3. 需要 cherry-pick 部分提交
   - Squash 后无法单独 cherry-pick

在本地手动 Squash

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 1. 切换到 feature 分支
git checkout feature/my-feature

# 2. 交互式 rebase
git rebase -i HEAD~5

# 3. 修改提交列表
pick abc1234 feat: 添加登录表单
squash def5678 feat: 实现登录 API
squash 9ab9012 feat: 添加错误处理
squash 0123456 fix: 修复表单验证
squash 7890abc fix: 修复登录状态

# 4. 编辑合并后的提交信息

# 5. 推送到远程
git push origin feature/my-feature --force-with-lease

Squash 合并的工作流程

flowchart TD
    A[功能开发] --> B[多个提交]
    B --> C[创建 PR]
    C --> D[代码审查]
    D --> E{审查通过?}
    E -->|否| F[修改代码]
    F --> B
    E -->|是| G{需要 Squash?}
    G -->|是| H[Squash 合并]
    G -->|否| I[普通合并]
    H --> J[main 分支只有一个提交]
    I --> K[main 分支保留所有提交]

团队约定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
## Squash 合并团队约定

### 默认使用 Squash 合并
- 所有 feature 分支 PR 默认 Squash 合并
- 保持 main 分支历史简洁

### 例外情况
- 大型重构需要保留详细历史
- 多个独立功能(应该拆分成多个 PR)

### 提交信息规范
- 标题:简明扼要描述功能
- 正文:列出主要改动点
- 关联 Issue:Closes #xxx

小贴士

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 配置 GitHub 默认 Squash 合并
# 在仓库设置中:
# Settings -> General -> Pull Requests
# 勾选 "Allow squash merging"
# 取消勾选 "Allow merge commits"(可选)

# 配置 Git 别名
git config --global alias.squash '!f() { git reset --soft HEAD~$1 && git commit -m "$(git log --format=%B --reverse HEAD..HEAD@{1})"; }; f'

# 使用:合并最近 5 个提交
git squash 5

记住:Squash 合并是主分支的"美颜相机"——去掉瑕疵,留下精华!


17.8 实战:向知名开源项目提交 PR 的完整经历

终于,我们要实战了!这次的目标是一个知名开源项目(以 React 为例),让我们完整走一遍从发现 bug 到 PR 合并的流程。

故事背景

:一个前端开发者,在使用 React 时发现了一个文档错误。

问题:React 官方文档中某个示例代码有 bug,会导致初学者困惑。

目标:修复这个文档错误,并向 React 提交 PR。

第一步:发现问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
## 发现问题的过程

1. 在阅读 React 文档时,发现 "Context" 章节的示例代码有问题
2. 示例代码中使用了废弃的 API
3. 复制代码到本地运行,确实报错
4. 确认是文档问题,不是自己的问题

## 确认是文档问题

- 在 GitHub 上搜索相关 Issue
- 发现没有人报告这个问题
- 决定自己修复并提交 PR

第二步:Fork 仓库

1
2
3
4
5
# 1. 在 GitHub 上访问 https://github.com/facebook/react

# 2. 点击右上角的 "Fork" 按钮

# 3. 等待 Fork 完成,现在你有了 https://github.com/your-username/react

第三步:克隆仓库

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 1. 克隆你 Fork 的仓库
git clone https://github.com/your-username/react.git
cd react

# 2. 添加上游仓库
git remote add upstream https://github.com/facebook/react.git

# 3. 验证远程仓库
git remote -v
# origin  https://github.com/your-username/react.git (fetch)
# origin  https://github.com/your-username/react.git (push)
# upstream  https://github.com/facebook/react.git (fetch)
# upstream  https://github.com/facebook/react.git (push)

第四步:创建分支

1
2
3
4
5
6
7
8
9
# 1. 获取上游最新代码
git fetch upstream

# 2. 基于上游 main 分支创建新分支
git checkout -b fix-context-docs upstream/main

# 3. 确认分支
git branch
# * fix-context-docs

第五步:修改代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 1. 找到需要修改的文件
# 假设是 documentation/docs/context.md

# 2. 打开文件并修改
vim documentation/docs/context.md

# 3. 修改内容示例:
# 把旧代码:
# const value = useContext(MyContext);
#
# 改成新代码:
# const value = useContext(MyContext);
# if (value === undefined) {
#   throw new Error('useContext must be used within a Provider');
# }

# 4. 本地验证修改
# 运行文档网站的本地服务器
npm run dev
# 访问 http://localhost:3000 查看修改效果

第六步:提交修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 1. 查看修改
git status
# modified: documentation/docs/context.md

# 2. 添加修改
git add documentation/docs/context.md

# 3. 提交
git commit -m "docs: 修复 Context 文档中的示例代码

- 添加了 useContext 的错误处理示例
- 帮助初学者理解 Provider 的必要性

Closes #xxxxx"

# 4. 推送到你的 Fork
git push origin fix-context-docs

第七步:创建 PR

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
## 在 GitHub 上创建 PR

1. 访问 https://github.com/your-username/react
2. 点击 "Compare & pull request"
3. 填写 PR 信息:

## PR 标题
docs: 修复 Context 文档中的示例代码

## PR 描述
```markdown
## 问题描述
Context 文档中的示例代码缺少错误处理,
导致初学者在 Provider 外部使用 useContext 时感到困惑。

## 改动内容
- 在示例代码中添加了 useContext 的错误处理
- 添加了注释说明 Provider 的必要性

## 截图
[修改前后的对比截图]

## 测试
- [x] 本地运行文档网站验证
- [x] 代码语法正确

## 关联 Issue
Fixes #xxxxx
  1. 点击 “Create pull request”

### 第八步:等待 Review

```markdown
## 等待 Review 的过程

### 第 1 天
- PR 创建成功
- CI 自动运行检查
- 等待维护者 review

### 第 3 天
- 收到维护者的评论:
  "感谢贡献!建议把错误处理改成警告而不是抛出异常,
   这样更符合 React 的风格。"

### 第 4 天
- 根据意见修改代码
- 提交新的 commit
- 回复评论:"已修改,请再次 review"

### 第 7 天
- 维护者批准 PR
- CI 检查通过
- 等待合并

第九步:根据 Review 修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 1. 本地修改
git checkout fix-context-docs

# 2. 编辑文件
vim documentation/docs/context.md
# 把 throw new Error 改成 console.warn

# 3. 提交修改
git add documentation/docs/context.md
git commit -m "docs: 根据 review 意见修改

- 将错误处理改为警告
- 更符合 React 风格"

# 4. 推送
git push origin fix-context-docs

第十步:PR 被合并

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
## 合并通知

"你的 PR 已被合并到 main 分支!
感谢你对 React 的贡献!"

## 后续

- PR 页面显示 "Merged"
- 你的 GitHub 个人资料上多了一个贡献记录
- React 的下一个版本会包含你的修改

完整流程图

flowchart TD
    A[发现文档问题] --> B[Fork 仓库]
    B --> C[克隆仓库]
    C --> D[创建分支]
    D --> E[修改代码]
    E --> F[本地验证]
    F --> G[提交修改]
    G --> H[推送分支]
    H --> I[创建 PR]
    I --> J[CI 检查]
    J --> K[代码审查]
    K --> L{需要修改?}
    L -->|是| M[修改代码]
    M --> N[推送更新]
    N --> K
    L -->|否| O[审查通过]
    O --> P[合并到 main]
    P --> Q[成为贡献者]

开源贡献的注意事项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
## ✅ 要做的

- [ ] 阅读项目的 CONTRIBUTING.md
- [ ] 遵循项目的代码规范
- [ ] 写清楚的 PR 描述
- [ ] 耐心等待 review
- [ ] 虚心接受反馈
- [ ] 感谢维护者的时间

## ❌ 不要做的

- [ ] 不读贡献指南就提交 PR
- [ ] PR 描述写 "修复 bug"
- [ ] 催促维护者 review
- [ ] 对 review 意见不耐烦
- [ ] 提交巨大的 PR

贡献后的维护

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 1. 更新本地仓库
git checkout main
git pull upstream main

# 2. 删除已合并的分支
git branch -d fix-context-docs
git push origin --delete fix-context-docs

# 3. 同步到你的 Fork
git push origin main

# 4. 继续贡献其他功能

经验总结

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
## 向开源项目贡献的经验

1. **从小处着手**
   - 先修复文档错误
   - 再修复小 bug
   - 最后添加新功能

2. **耐心很重要**
   - 维护者可能很忙
   - review 可能需要几天甚至几周
   - 不要催促

3. **沟通是关键**
   - 写清楚的 PR 描述
   - 及时回复 review 意见
   - 不懂就问

4. **持续贡献**
   - 一次贡献只是开始
   - 持续贡献才能建立信任
   - 最终可能成为维护者

小贴士

1
2
3
4
5
6
7
# 配置 Git 别名,方便开源贡献
git config --global alias.fork '!f() { git remote add upstream $1; }; f'
git config --global alias.sync '!git fetch upstream && git checkout main && git rebase upstream/main'

# 使用
git fork https://github.com/facebook/react.git
git sync

记住:向开源项目贡献不只是修复 bug,更是融入社区、学习成长的过程!


第17章完

最后修改 April 3, 2026: 新增 git 教程 (c56c038)