Git Worktree 多分支并行开发:一套不互相污染的标准流程
很多人第一次想做“多功能并行开发”时,直觉都是多开几个分支,然后在一个仓库目录里来回 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__*.dbsnapshot_*.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
然后再重新走合并流程。
只改了前端,要不要重启后端
通常不需要。很多时候静态资源重新构建并部署成功后,刷新缓存就够了。