很多人第一次想做“多功能并行开发”时,直觉都是多开几个分支,然后在一个仓库目录里来回 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 目录里,比如:

/Users/kuangyongjie/worktrees/hotspot-ff

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

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

先准备一下 worktree 根目录:

mkdir -p /Users/kuangyongjie/worktrees

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

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

正常情况下,最后应该看到:

codex/<feature-name>

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

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

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

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

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

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

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

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

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

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

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

提交的时候,我基本不用 git add .

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

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

像这些东西就很常见:

  • __pycache__
  • *.db
  • snapshot_*
  • .DS_Store

我更倾向于这样提:

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

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

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

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

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 本身,而是你要确认自己真的站在主目录、主分支上。

如果你遇到:

MERGE_HEAD exists

通常说明上次 merge 没收干净,可以先:

git merge --abort

然后再重新执行 pull 和 merge。

如果你后面还要推远端,再补一步:

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

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

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

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

比如:

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 里启动服务,比如:

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

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

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 挂了,而是远端名写错了。先跑:

git remote -v

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

MERGE_HEAD exists

说明上次 merge 没结束。先:

git merge --abort

然后再重新走合并流程。

只改了前端,要不要重启后端

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