# Git Worktree 多分支并行开发：一套不互相污染的标准流程

> Canonical: https://vxi.me/posts/git-worktree-parallel-development/
> Locale: zh-cn
> Published: 2026-03-25
> Updated: 2026-03-25
> Markdown: https://vxi.me/posts/git-worktree-parallel-development.md

很多人第一次想做“多功能并行开发”时，直觉都是多开几个分支，然后在一个仓库目录里来回 `checkout`。短时间看起来还能用，但只要你一边改功能、一边跑服务、一边修线上问题，很快就会发现这套方式特别容易串。

我后来基本固定成了另一种做法：主目录只保留稳定分支，真正开发时用 `git worktree` 给每个功能开一个独立目录。这样你在同一台机器上可以同时维护多个功能分支，而且目录、进程、构建产物基本都隔开了。

如果你现在在搜 Git 多分支并行开发、Git worktree 用法、Worktree 标准流程，这篇文章就是围绕这些问题写的。

## 为什么我最后选了 Git Worktree，而不是反复切分支

真正麻烦的从来不是“怎么创建一个新分支”，而是你怎么确保自己测试的就是这个分支、服务跑的也是这个目录、最后提交时不会把别的功能残留一起带进去。

在一个仓库目录里反复切分支，最容易出现几类问题：

- 你以为自己在测新功能，结果服务还是从旧目录启动的
- 某个功能留下来的构建产物，污染了另一个分支
- `git add .` 一把梭，把根本不该提交的文件也带上去了
- 一边修线上 bug，一边改新功能，目录状态越来越乱

`git worktree` 的好处就在这里。它不是让你“更快创建分支”而已，而是让每个功能拥有自己单独的工作目录。对我来说，这个隔离感比什么都重要。

## 我自己固定下来的目录约定

为了避免后面越用越乱，我会先把几个约定定死：

- 主仓库目录：`/Users/kuangyongjie/charting_library`
- worktree 根目录：`/Users/kuangyongjie/worktrees`
- 分支命名：`codex/<feature-name>`
- 主分支：`master`

也就是说，主目录只负责保持稳定和合并。真正开发一个功能时，我会把它放到一个新的 worktree 目录里，比如：

```text
/Users/kuangyongjie/worktrees/hotspot-ff
```

这个约定看起来很小，但实际很有用。因为你只要看到路径，就知道当前到底在主仓库，还是在某个功能分支目录。

## 标准做法其实就几步，但每一步都别省

先准备一下 worktree 根目录：

```bash
mkdir -p /Users/kuangyongjie/worktrees
```

然后从主仓库创建一个独立 worktree 和新分支：

```bash
REPO=/Users/kuangyongjie/charting_library
WT=/Users/kuangyongjie/worktrees/<feature-name>
BRANCH=codex/<feature-name>
BASE=master

# 如果远端可用，先更新 master
# git -C "$REPO" pull --rebase central "$BASE"

git -C "$REPO" worktree add "$WT" -b "$BRANCH" "$BASE"
cd "$WT"
git branch --show-current
```

正常情况下，最后应该看到：

```text
codex/<feature-name>
```

这一步很关键，因为它明确告诉你：从现在开始，这个功能的代码、构建、测试，都应该只发生在 `"$WT"` 里。

## 只在分支目录里开发和测试，这条规则最好别破

后面无论你改前端、跑后端，还是做本地验证，都只在这个 worktree 目录里做。

比如前端项目在子目录里，你就进这个分支目录再构建：

```bash
cd "$WT/trendradar-service/trendradar-web"
npm run build
# 生产混淆构建
# npm run build:multi-layer
```

我自己踩过一个很常见的坑，就是代码明明改了，页面却还是旧效果。后来一查，不是代码没生效，而是服务压根跑在另一个目录里。

所以只要出现“我明明改了但没变化”，我第一反应不是怀疑框架，而是先查当前进程的 `cwd`。

```bash
lsof -nP -iTCP:<port> | sed -n '1,5p'
ps -p <pid> -o pid,ppid,command
lsof -p <pid> | rg "cwd"
```

你最后必须确认 `cwd` 指向的是：

```text
/Users/kuangyongjie/worktrees/<feature-name>/...
```

不然你测试的东西很可能根本不是这个分支。

## 提交的时候，我基本不用 `git add .`

这一点我会故意保守一点。

只要是 worktree 并行开发，我提交前几乎都会只加目标文件，而不是直接 `git add .`。因为一旦你本地有运行产物、缓存文件、快照文件，这一把很容易把脏东西也带进去。

像这些东西就很常见：

- `__pycache__`
- `*.db`
- `snapshot_*`
- `.DS_Store`

我更倾向于这样提：

```bash
cd "$WT"
git add <file1> <file2> <file3>
git status --short
git commit -m "feat/fix: <summary>"
```

先看一眼 `git status --short` 再 commit，这个动作虽然小，但能挡掉很多低级错误。

## 合并回主分支时，只在主目录做

功能做完以后，合并动作我只会在主仓库目录执行，不会在 worktree 里顺手 merge。

```bash
cd /Users/kuangyongjie/charting_library
git branch --show-current   # 必须是 master

# 如远端可用，先更新
# git pull --rebase central master

git merge --no-ff codex/<feature-name>
```

这里的重点其实不是 `merge --no-ff` 本身，而是你要确认自己真的站在主目录、主分支上。

如果你遇到：

```text
MERGE_HEAD exists
```

通常说明上次 merge 没收干净，可以先：

```bash
git merge --abort
```

然后再重新执行 pull 和 merge。

如果你后面还要推远端，再补一步：

```bash
cd /Users/kuangyongjie/charting_library
git push <remote> master
```

顺手提醒一下，你这台机器上常见远端名是 `central`，不一定是 `origin``。这类问题通常是环境差异，不是 Git Worktree 本身的问题。

## 什么情况下我会用 Fast-forward，而不是强行留 merge 记录

如果你的要求是“分支单独启动测试，通过后再合并，而且不生成 merge 记录”，那就可以走 fast-forward 的思路。

比如：

```bash
cd /Users/kuangyongjie/charting_library
git checkout master
git pull --rebase central master

git worktree add /Users/kuangyongjie/worktrees/hotspot-ff -b codex/hotspot-ff master
```

然后只在这个分支 worktree 里启动服务，比如：

```bash
cd /Users/kuangyongjie/worktrees/hotspot-ff/trendradar-service
./venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 9544
```

再开一个终端，确认当前进程的工作目录确实就是这个分支：

```bash
lsof -nP -iTCP:9544 -sTCP:LISTEN
ps -p <PID> -o pid,ppid,command
lsof -a -p <PID> -d cwd -Fn
```

这个检查动作非常值，因为很多“分支验证通过”的结论，最后其实是建立在测错目录上的。

## 我现在基本固定下来的几条规则

如果你也准备长期用 Worktree，我建议至少把下面几条变成硬规则：

- 一个功能一个 worktree 目录
- 一个对话或一个任务只对应一个 worktree
- 开工先跑 `git branch --show-current`
- 提交前先看 `git status --short`
- 合并只在主目录 `master` 执行

这几条听起来很普通，但真正能让并行开发不乱掉，靠的就是这些基础动作。

## 常见问题

### 本地页面还是旧效果

先别急着怀疑代码。

先查服务进程的 `cwd` 是不是在目标 worktree，再确认你是不是已经在那个分支目录里重新构建过前端产物。这个问题特别常见。

### `fatal: 'origin' does not appear to be a git repository`

一般不是 Git 挂了，而是远端名写错了。先跑：

```bash
git remote -v
```

然后按实际远端名 push。你这边常见的是 `central`。

### `MERGE_HEAD exists`

说明上次 merge 没结束。先：

```bash
git merge --abort
```

然后再重新走合并流程。

### 只改了前端，要不要重启后端

通常不需要。很多时候静态资源重新构建并部署成功后，刷新缓存就够了。
