第四章:Git 工作流与 PR 自动化
学习目标
- 让 Claude 接管你的整个 Git 工作流:分支、编码、提交、PR -- 不需要你自己输入 git 命令
- 构建一个真实功能(暗黑模式切换),使用正确的分支策略和原子提交
- 用 Git Worktree 实现真正的并行开发,多个 Claude 会话同时运行
- 故意搞砸然后用
/rewind恢复 -- 建立信心,敢让 Claude 尝试大胆的改动 - 用管道模式在每次合并前做即时代码审查
附录链接:A05 工具调用内部机制 解释 Claude 在需要 git 信息时如何在 Read 和 Bash 之间选择。A01 提示工程 介绍如何写出能产生好 commit message 的提示。
核心概念
Claude 的 Git 集成是深度的
Claude Code 不只是运行 git 命令。它理解你的代码变更,通过分析 diff 写 commit message,选择性地暂存文件(从不用 git add -A),生成带测试计划的结构化 PR 描述。
完整循环,全部通过对话完成:
1. 创建分支 <- Claude 按你的命名规范取名
2. 写代码 <- Claude 的核心能力
3. 暂存变更 <- Claude 选择特定文件,跳过 .env 和密钥
4. 提交 <- Claude 读 diff 写 commit message
5. 创建 PR <- Claude 生成标题、摘要、测试计划
6. 代码审查 <- Claude 也能审查别人的 PR为什么"从不用 git add -A"? 因为 git add -A 会暂存所有东西 -- 包括 .env 文件、凭证、大型二进制文件和临时文件。Claude 总是在检查过变更后使用 git add <specific-files>。这是安全实践,不只是风格偏好。
GSD 的原则在这里适用:每次提交一个逻辑变更,原子任务。当你给出聚焦的指令时,Claude 自然会遵循这个原则。
Checkpoint 系统
每次 Claude 编辑文件,它都会创建一个 Git checkpoint。就像游戏里的自动存档。
编辑 1: 添加暗黑模式 CSS -> checkpoint #1
编辑 2: 添加切换组件 -> checkpoint #2
编辑 3: 接入状态管理 -> checkpoint #3
编辑 4: 重构(搞砸了) -> checkpoint #4
编辑 4 不行:
/rewind -> 选择 checkpoint #3
-> 一切回到编辑 3 之后的正常状态这是安全网。 你可以告诉 Claude "试试激进的重构"而不用担心风险。如果不行,/rewind 就回去了。Checkpoint 系统底层使用 Git 的暂存区和 stash 机制,所以你的实际分支历史保持干净。
Git Worktree:真正的并行
Git worktree 从同一个仓库创建多个工作目录,每个在不同的分支上。结合 Claude Code,你可以运行并行会话来处理完全独立的功能。
my-app/ <- main 分支
my-app-feature-dark/ <- worktree: feature/dark-mode
my-app-feature-api/ <- worktree: feature/api-v2
三个终端,三个 Claude 会话,零干扰。这不是切换分支。 这是独立的目录,有独立的文件状态。一个 Claude 会话不可能误覆盖另一个的工作。Claude Code 原生支持这种模式 -- 当你在 worktree 目录中启动 claude 时,它会自动检测 Git 上下文。
Demo 8:用正确的 Git 工作流构建暗黑模式
目标
给一个简单的 Web 应用添加暗黑模式切换,使用正确的 Git 实践:功能分支、原子提交、干净地合并回主分支。
步骤
1. 创建基础项目
mkdir -p ~/claude-demos/demo-08 && cd ~/claude-demos/demo-08
git init
git branch -M main
mkdir -p src
cat > src/index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo App</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>My App</h1>
<nav>
<a href="#home">Home</a>
<a href="#about">About</a>
</nav>
</header>
<main>
<p>Welcome to the demo app.</p>
</main>
<script src="app.js"></script>
</body>
</html>
EOF
cat > src/styles.css << 'EOF'
body {
font-family: system-ui, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background: #ffffff;
color: #1a1a1a;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 1rem;
margin-bottom: 2rem;
}
nav a {
margin-left: 1rem;
color: #0066cc;
text-decoration: none;
}
EOF
cat > src/app.js << 'EOF'
console.log('App initialized');
EOF
cat > CLAUDE.md << 'EOF'
# Demo App
## Key Commands
- Open in browser: open src/index.html (macOS) or start src/index.html (Windows)
## Standards
- Vanilla HTML/CSS/JS, no frameworks
- CSS custom properties (variables) for theming
- Commit messages: Conventional Commits, English
- One logical change per commit
EOF
git add -A
git commit -m "feat: initial app with basic layout and styling"2. 让 Claude 构建暗黑模式
claudeI want to add dark mode to this app. Here's the plan:
1. Create a feature/dark-mode branch
2. Add CSS custom properties for colors (light theme as default)
3. Add a dark theme using a [data-theme="dark"] attribute on <html>
4. Add a toggle button in the header
5. Write the JS to toggle themes and persist the choice in localStorage
6. Make separate commits for each logical step
Go ahead.你实际会看到的内容:
> [Your prompt above]
╭─ Bash ──────────────────────────────────────────────╮
│ git checkout -b feature/dark-mode │
╰─────────────────────────────────────────────────────╯
> y
Switched to a new branch 'feature/dark-mode'
╭─ Read ──────────────────────────────────────────────╮
│ src/styles.css │
╰─────────────────────────────────────────────────────╯
> a
[Reading src/styles.css... 20 lines]
I'll start by refactoring the hardcoded colors to CSS custom properties.
╭─ Edit ──────────────────────────────────────────────╮
│ src/styles.css │
│ Replace hardcoded colors with CSS variables │
╰─────────────────────────────────────────────────────╯
> y
[Edited src/styles.css]
╭─ Bash ──────────────────────────────────────────────╮
│ git add src/styles.css │
│ git commit -m "refactor: use CSS custom properties │
│ for theming" │
╰─────────────────────────────────────────────────────╯
> y
[main 2a1b3c4] refactor: use CSS custom properties for theming
Now adding the dark theme color scheme...
╭─ Edit ──────────────────────────────────────────────╮
│ src/styles.css │
│ Add [data-theme="dark"] rules │
╰─────────────────────────────────────────────────────╯
> y
[...]
╭─ Bash ──────────────────────────────────────────────╮
│ git add src/styles.css │
│ git commit -m "feat: add dark theme color scheme" │
╰─────────────────────────────────────────────────────╯
> y
[feature/dark-mode 5d6e7f8] feat: add dark theme color scheme
Now I'll add the toggle button and JavaScript...
╭─ Edit ──────────────────────────────────────────────╮
│ src/index.html │
│ Add toggle button in header │
╰─────────────────────────────────────────────────────╯
> y
╭─ Edit ──────────────────────────────────────────────╮
│ src/app.js │
│ Add theme toggle with localStorage persistence │
╰─────────────────────────────────────────────────────╯
> y
╭─ Bash ──────────────────────────────────────────────╮
│ git add src/index.html src/app.js │
│ git commit -m "feat: add dark mode toggle with │
│ localStorage persistence" │
╰─────────────────────────────────────────────────────╯
> y
[feature/dark-mode 9a0b1c2] feat: add dark mode toggle...
Done! Three atomic commits on the feature/dark-mode branch.3. 验证工作
git log --oneline --all --graph预期输出:
$ git log --oneline --all --graph
* 9a0b1c2 (HEAD -> feature/dark-mode) feat: add dark mode toggle with localStorage persistence
* 5d6e7f8 feat: add dark theme color scheme
* 2a1b3c4 refactor: use CSS custom properties for theming
* 1234567 (main) feat: initial app with basic layout and styling4. 合并回去
还在 Claude 会话中:
Switch back to main and merge feature/dark-mode. Then show me the final git log.刚才发生了什么?
Claude 做对了什么:
- 创建了遵循 CLAUDE.md 命名规范的功能分支
- 做了 3 个原子提交 -- 每个是一个逻辑变更,可独立审查
- 通过读取实际 diff 生成 commit message
- 选择性暂存文件 -- 始终
git add <file>,从不git add -A - 添加了
Co-Authored-By尾部来表明 AI 的参与
Demo 9:用 Worktree 并行开发
目标
同时处理两个功能:前端增强和后端 API,在互不干扰的并行 Claude 会话中进行。
步骤
1. 从 Demo 8 创建 Worktree
cd ~/claude-demos/demo-08
git checkout main
# 为两个功能创建 worktree
git worktree add ../demo-08-search feature/search
git worktree add ../demo-08-api feature/api-endpoints这会创建什么:
$ git worktree list
/home/user/claude-demos/demo-08 1234567 [main]
/home/user/claude-demos/demo-08-search 1234567 [feature/search]
/home/user/claude-demos/demo-08-api 1234567 [feature/api-endpoints]2. 终端 1:前端功能
打开一个终端:
cd ~/claude-demos/demo-08-search
claudeAdd a search feature to this app:
1. Add a search input in the header
2. Add some sample content paragraphs to the main section
3. Implement client-side search that highlights matching text and hides non-matching paragraphs
4. Commit when done3. 终端 2:后端功能
同时打开第二个终端:
cd ~/claude-demos/demo-08-api
claudeAdd a simple API backend:
1. Create a server.js using Node's built-in http module (no Express needed)
2. Implement GET /api/status that returns { status: "ok", uptime: process.uptime() }
3. Implement GET /api/config that returns the app configuration
4. Add a start script to package.json (create it if it doesn't exist)
5. Commit when done4. 两个会话同时运行
两个 Claude 会话工作的时候,彼此不知道对方的存在。它们在完全独立的目录中,有独立的文件状态。不会因为并发编辑产生合并冲突。
5. 合并两个功能
cd ~/claude-demos/demo-08
git merge feature/search
git merge feature/api-endpoints
git log --oneline --all --graph6. 清理
git worktree remove ../demo-08-search
git worktree remove ../demo-08-api刚才发生了什么?
为什么用 worktree 而不是切换分支?
| 方式 | 问题 |
|---|---|
| 切换分支 | 切换前必须 stash 或 commit。同一时间只有一个会话能工作。有忘记自己在哪个分支上的风险。 |
| Git worktree | 每个功能有自己的目录。多个 Claude 会话真正并行工作。天然完全隔离。 |
这种模式在第九章 Agent 团队中变得至关重要,届时你将有 3 个以上的 Claude agent 同时在同一个仓库上工作。
Demo 10:故意搞砸,然后 /rewind
目标
故意让 Claude 做一个失败的重构,然后用 checkpoint 系统恢复。这会让你建立信心,敢让 Claude 尝试大胆的改动。
步骤
1. 准备一个正常工作的应用
mkdir -p ~/claude-demos/demo-10 && cd ~/claude-demos/demo-10
git init
cat > calculator.py << 'EOF'
"""A simple calculator module with history tracking."""
class Calculator:
def __init__(self):
self.history = []
def add(self, a, b):
result = a + b
self.history.append(f"{a} + {b} = {result}")
return result
def subtract(self, a, b):
result = a - b
self.history.append(f"{a} - {b} = {result}")
return result
def multiply(self, a, b):
result = a * b
self.history.append(f"{a} * {b} = {result}")
return result
def divide(self, a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
result = a / b
self.history.append(f"{a} / {b} = {result}")
return result
def get_history(self):
return list(self.history)
def clear_history(self):
self.history.clear()
if __name__ == "__main__":
calc = Calculator()
print(calc.add(10, 5))
print(calc.subtract(10, 3))
print(calc.multiply(4, 7))
print(calc.divide(15, 3))
print("History:", calc.get_history())
EOF
cat > test_calculator.py << 'EOF'
"""Tests for the calculator module."""
import pytest
from calculator import Calculator
def test_add():
calc = Calculator()
assert calc.add(2, 3) == 5
def test_subtract():
calc = Calculator()
assert calc.subtract(10, 4) == 6
def test_multiply():
calc = Calculator()
assert calc.multiply(3, 7) == 21
def test_divide():
calc = Calculator()
assert calc.divide(10, 2) == 5.0
def test_divide_by_zero():
calc = Calculator()
with pytest.raises(ValueError):
calc.divide(10, 0)
def test_history():
calc = Calculator()
calc.add(1, 2)
calc.subtract(5, 3)
history = calc.get_history()
assert len(history) == 2
assert "1 + 2 = 3" in history[0]
EOF
git add -A
git commit -m "feat: calculator with tests"2. 让 Claude 做渐进式修改
claude先来一个好的改动:
Add power() and sqrt() methods to the Calculator class, with history tracking and tests. Run the tests to make sure everything passes.> [prompt above]
[Claude edits calculator.py, adds power() and sqrt()]
[Claude edits test_calculator.py, adds new tests]
╭─ Bash ──────────────────────────────────────────────╮
│ python -m pytest test_calculator.py -v │
╰─────────────────────────────────────────────────────╯
> y
test_calculator.py::test_add PASSED
test_calculator.py::test_subtract PASSED
test_calculator.py::test_multiply PASSED
test_calculator.py::test_divide PASSED
test_calculator.py::test_divide_by_zero PASSED
test_calculator.py::test_history PASSED
test_calculator.py::test_power PASSED
test_calculator.py::test_sqrt PASSED
8 passed ✓好的。测试通过。Checkpoint 已创建。
再来一个好的改动:
Add a memory feature: store_memory(slot_name, value) and recall_memory(slot_name). With tests.好的。测试仍然通过。又一个 checkpoint。
现在来个冒险的:
Refactor the entire Calculator class to use a plugin architecture. Each operation (add, subtract, etc.) should be a separate plugin class that registers with the calculator. This is a major refactor -- go for it.这是一个大改动。Claude 会重写大部分文件。也许测试会挂。也许重构过度复杂化了。这都无所谓。
3. 用 /rewind 恢复
/rewind你会看到:
> /rewind
Available checkpoints:
[1] feat: calculator with tests (initial)
[2] Added power() and sqrt() methods ← 2 min ago
[3] Added memory feature ← 1 min ago
[4] Plugin architecture refactor ← just now
Rewind to which checkpoint? (1-4): 3选择 checkpoint 3 -- power、sqrt 和 memory 都正常工作的状态。
4. 验证恢复
Run pytest to confirm everything works╭─ Bash ──────────────────────────────────────────────╮
│ python -m pytest test_calculator.py -v │
╰─────────────────────────────────────────────────────╯
10 passed ✓测试通过。激进的重构已经消失了。Power、sqrt 和 memory 完好无损。
刚才发生了什么?
核心教训:/rewind 意味着你永远不需要害怕让 Claude 尝试大胆的东西。"试试函数式编程风格。""用完全不同的模式重写。""如果用组合代替继承会怎样?"放心大胆地问。不行就 /rewind,几秒钟回到原样。
常见问题排查
合并冲突
$ git merge feature-branch
Auto-merging src/styles.css
CONFLICT (content): Merge conflict in src/styles.css
Automatic merge failed; fix conflicts and then commit the result.解决:在有冲突的目录中启动 Claude:
claudeResolve the merge conflict in src/styles.css. Keep functionality from both sides.Claude 读取冲突标记(<<<<<<<、=======、>>>>>>>),理解两个分支的意图,生成干净的合并结果。它在这方面出奇地好,因为它能看到两边变更的完整上下文。
/rewind 回退太多
如果你不小心 rewind 到了一个比预期更早的状态,检查 git reflog:
git reflog
# 找到你想要的状态的 commit hash
git checkout <hash> -- <file>Worktree 分支已存在
$ git worktree add ../demo-api feature/api
fatal: 'feature/api' is already checked out at '/path/to/other'解决:要么先移除已有的 worktree(git worktree remove <path>),要么用新的分支名(feature/api-v2)。
Claude 暂存了不想要的文件
如果 Claude 暂存了你不想提交的文件:
Unstage that file. I don't want credentials.json in the commit.或者手动:git reset HEAD credentials.json
深入探索
管道模式做即时代码审查
这是最被低估的功能之一:
# 审查未提交的变更
git diff | claude -p "Review these changes. Flag security issues, performance concerns, and style violations."
# 审查特定分支
git diff main..feature/dark-mode | claude -p "Review this PR. Would you approve it?"
# 审查最后一次提交
git show | claude -p "Rate this commit from 1-10 on: message quality, change cohesion, and test coverage."
# 从提交生成 changelog
git log --oneline v1.0..v2.0 | claude -p "Generate a user-facing changelog from these commits. Group by feature/fix/breaking."管道模式审查输出示例:
$ git diff | claude -p "Review these changes for security issues"
## Security Review
**1 issue found:**
- **HIGH**: `src/api.js:42` — User input is passed directly to `eval()`.
This is a code injection vulnerability. Use `JSON.parse()` instead
if you're parsing JSON, or a sandboxed evaluator if you genuinely
need dynamic evaluation.
**No other security concerns.** The rest of the changes (CSS updates,
README edits) are safe.管道模式是非交互的 -- Claude 读 stdin,回答,退出。完美适合集成到脚本和 CI 管道中。更多用法见第五章 Hooks。
Claude 怎么写 Commit Message
Claude 不是猜的。它遵循一个流程:
- 读
git diff --staged看到底改了什么 - 查
git log匹配项目现有的提交风格 - 把相关文件分组到单个提交中
- 跳过
.env、凭证等敏感文件 - 添加
Co-Authored-By: Claude ...尾部
精心编写的 commit message 示例:
feat(auth): add JWT token refresh endpoint
- Add POST /auth/refresh endpoint with rate limiting
- Persist refresh tokens in separate table with encryption
- Add audit logging for token refresh events
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>PR 创建
如果你有 GitHub remote 并且装了 gh CLI:
Create a PR from the current branch to main. Include a descriptive title, a summary of changes with context, and a test plan.Claude 使用 gh pr create 并生成结构化的 PR 正文。PR 描述会引用改了什么和为什么改,不只是列出文件。
知识检测
练习:纯对话 Git 工作流
任务
用纯对话方式完成整个 Git 工作流。你不能自己输入任何 git 命令。
- 初始化一个新的 Git 仓库
- 创建项目:一个把 Markdown 转 HTML 的 Node.js CLI 工具
- 提交初始代码
- 创建
feature/frontmatter分支 - 添加 YAML frontmatter 解析支持
- 提交并写好 commit message
- 切回 main
- 合并功能分支
- 显示最终的 git log
成功标准
- [ ] 没有手动输入任何 git 命令
- [ ] 至少有 3 次提交,使用 Conventional Commits 格式
- [ ] 功能分支被正确创建和合并
- [ ]
git log --graph显示了分支历史 - [ ] Claude 使用了选择性暂存(不是
git add -A)
提示
Initialize a git repo, create a Node.js CLI tool in index.js that reads
a markdown file and converts it to HTML using a simple parser (no external deps).
Add a package.json with a bin entry. Commit everything.Create a feature/frontmatter branch. Add YAML frontmatter parsing -- when the
markdown starts with --- delimited YAML, extract it as metadata and include
it as <meta> tags in the HTML output. Commit.Switch to main, merge feature/frontmatter, show git log --graph --oneline.本章小结
- Claude 处理完整的 Git 生命周期:分支、编码、暂存、提交、PR、审查
- 原子提交是标准。每次提交一个逻辑变更,选择性暂存,有意义的 message。
- Checkpoint 在每次编辑时自动保存。
/rewind是你的撤销按钮 -- 让你敢于尝试大胆的改动。 - Git Worktree 实现真正的并行开发。多个 Claude 会话,多个功能,零冲突。
- 管道模式(
git diff | claude -p)是即时代码审查。每次 PR 前都用它。 - Claude 通过读 diff 写 commit message,不是猜的。它会匹配你项目现有的风格。
- 合并冲突发生时,Claude 通过理解两个分支的意图来解决。
你已经完成了入门阶段。
你学到的一切 -- 工具调用、CLAUDE.md、上下文管理、Git 工作流 -- 在阶段性大项目中融为一体。不是 todo 应用。你要构建一个你会真正使用的工具。
或者直接跳到阶段二:进阶篇 - 第五章:Hooks 系统,开始自动化 Claude Code 本身。