别再手敲 git checkout -b 了,我把团队分支规范做成了 CLI
大家好😁。
最近我在团队里做了一个很小的工具,叫 pumpp。
它解决的问题听起来特别不起眼,甚至有点无聊:
每周发布的时候,别再手敲 git checkout -b release/xxx 了。
你可能会说,这也值得做个工具?
说实话,一开始我也觉得不值得。切个分支嘛,谁不会。
git checkout -b release/1.8.0-20260425 main
看着很简单,对吧。
但这个命令,如果你们团队每周都要敲,如果每个人都要敲,如果分支名还必须符合统一规范,那它就不是一个简单命令了。
它变成了一件反复消耗注意力的小事。
而工程里最烦人的,往往就是这种小事。
事情是从每周发版开始的
我们团队有固定的发布节奏,每周都要创建发布分支。
分支名大概长这样:
release/1.8.0-20260425
里面有几个信息:
-
release/前缀 -
package.json里的版本号 - 当天日期
- 从
main切出来
规范写得很清楚。
文档里有,群公告里也发过,甚至有人把示例命令贴在 README 里。
问题是,规范写在文本里,不等于规范真的被执行。
每次到发版的时候,还是会有人问:
这周 release 分支要带日期吗?
是
release/1.8.0-20260425,还是release/v1.8.0-20260425?
从
main切,还是从当前分支切?
hotfix 的格式是不是跟 release 一样?
这些问题都不难,但它们每周都会出现。
更烦的是,哪怕大家都知道规范,也还是会手滑。
有人少写日期,有人把 / 写成 -,有人复制上周命令忘了改版本号,有人本来想切 hotfix,结果前缀写成了 fix。
这些错误都不是什么大事故。
但它们会不断打断流程。
你要删掉错分支,重新创建,确认有没有推到远端,确认 CI 有没有被触发,确认别人有没有基于那条分支拉代码。
一来一回,十分钟没了。
我有时候觉得,工程效率很多时候不是被大问题拖垮的,而是被这种小摩擦磨掉的。
文本约束的问题
团队当然可以继续靠文档约束。
比如写一段:
发布分支格式:release/{version}-{date}
功能分支格式:feature/{username}-{desc}
热修分支格式:hotfix/{username}-{desc}
然后希望大家每次都照着来。
但说真的,这种约束很脆。
新同学不知道文档在哪,老同学觉得自己记得,忙的时候没人愿意打开文档确认,CI 报错之后大家才想起来:「哦对,我们规范不是这么写的」。
这就很像早期团队靠口头约定代码格式。
「我们缩进用两个空格」
「import 要排序」
「变量名不要随便缩写」
听起来都对,但只要没有 ESLint / Prettier,最后一定会变成 review 里互相提醒。
分支命名也是一样。
靠人记住,是最不稳定的方案。
如果一条规则真的重要,就应该让工具来执行它。
这就是 pumpp 的出发点。
我想要的不是 git wrapper
我不想做一个复杂的 Git 客户端。
Git 已经够强了,也够复杂了。
pumpp 想解决的是更窄的一件事:
把团队分支命名规范写进配置文件,再用一个统一命令创建分支。
也就是这两步:
- 项目里放一份
pumpp.config.ts - 团队成员通过
pnpm branch或pumpp创建分支
先看配置。
import { definePumpConfig } from 'pumpp-cli'
export default definePumpConfig({
base: 'main',
types: {
release: {
pattern: 'release/{version}-{date}',
},
feature: {
pattern: 'feature/{username}-{desc?}',
base: 'HEAD',
},
hotfix: {
pattern: 'hotfix/{username}-{desc?}',
},
},
})
这个配置的核心是 pattern。
release/{version}-{date} 的意思就是:
-
{version}从 manifest 里读,默认是package.json的version -
{date}用当天日期,格式是YYYYMMDD - 最后拼出一条合法分支名
但这里还有一个字段,我觉得跟 pattern 一样重要。
base。
分支名写对了,但从错的地方切出来,后面一样会很痛。
比如 release 分支应该永远从 main 切,这样它代表的是主干某个明确状态。如果有人在自己的 feature 分支上手滑敲了一句:
git checkout -b release/1.8.0-20260425
那这条 release 分支里可能混进一堆还没合并的实验代码。
名字看起来完全正确。
但内容已经错了。
这比名字错还麻烦,因为它不一定第一时间暴露。
所以 pumpp 里 base: 'main' 的意思不是装饰字段,而是在说:
这个类型的分支,只能从 main 切。
你也可以在具体类型上覆盖它。
比如 release / hotfix 都从 main 切,但 feature 想从当前所在分支切,就写:
types: {
release: { pattern: 'release/{version}-{date}', base: 'main' },
hotfix: { pattern: 'hotfix/{username}-{desc?}', base: 'main' },
feature: { pattern: 'feature/{username}-{desc?}', base: 'HEAD' },
}
HEAD,或者简写 '.',表示「我当前 checkout 的分支」。
这在一些团队里很有用。比如你正在 dev 上做一组相关改动,想从当前上下文里切一条 feature,而不是每次都从 main 开始。
但 release 这种分支,我个人建议还是写死 main。
发布分支从哪来,是流程边界,不要靠手感。
顺手说一句,pumpp 会在真正创建分支前校验 base,它必须是本地存在的分支。origin/main、tag、commit SHA 这种都不会被当成合法 base。这个限制看起来有点严格,但目的很简单,别让大家以为自己从远端主干切了,结果实际用的是一个含糊的引用。
然后在 package.json 里加一个团队入口。
{
"scripts": {
"branch": "pumpp",
"branch:release": "pumpp release",
"branch:feature": "pumpp feature",
"branch:hotfix": "pumpp hotfix"
}
}
之后团队成员不需要记 git checkout -b 那一长串,也不需要记 pattern。
直接:
pnpm branch
它会进入交互模式,让你选择分支类型。
或者更直接一点:
pnpm branch:release
创建发布分支。
pnpm branch:feature --desc login
创建功能分支。
这才是我更推荐的用法。
不是每个人全局安装一个命令,然后自己凭印象敲;而是项目把 branch 入口写在 scripts 里,团队只认这个入口。
像跑测试一样。
你不会要求新人记住底层测试命令到底是 vitest run 还是 jest --runInBand,你会告诉他:
pnpm test
切分支也应该一样。
创建分支时,它会先停下来问你
执行 pnpm branch:release 后,pumpp 会根据配置算出分支名。
比如:
release/1.8.0-20260425
然后它不是直接冲上去创建,而是先给你一个确认菜单:
? Branch name: release/1.8.0-20260425
❯ ✔ Accept Create this branch as-is
✎ Edit Modify before creating
✖ Cancel Abort, do not touch the repo
这个菜单是我特意做的。
因为本地开发不是 CI。
本地会有很多临时情况,比如这次 release 想带一个 rc1 后缀,或者 feature 描述想临时改一下。
所以 pumpp 给了三个选择:
- Accept,按当前分支名创建
- Edit,在已经预填好的分支名上直接修改
- Cancel,干净退出,不动仓库
Edit 之后还会重新跑 git check-ref-format 和重名检查。
这点很重要。
工具不能一边说自己约束规范,一边在用户手动改名后就放飞自我。
交互不是花活,是为了少记东西
再看 feature 分支。
默认 pattern 是:
feature/{username}-{desc?}
{username} 会从 Git 用户名、环境变量、系统用户名里取一个,然后 slug 成适合分支名的形式。
{desc?} 是可选描述。
你可以直接传:
pnpm branch:feature --desc oauth-refresh
得到:
feature/alice-oauth-refresh
如果你不传 --desc,在支持交互的终端里,pumpp 会问你要不要填描述,并且显示实时预览。
这个预览不是炫技。
它解决的是一个很小但真实的问题:
我在输入描述的时候,想知道最终分支名长什么样。
尤其是描述里有空格、大写、下划线的时候,比如你输入:
Fix Login Bug
pumpp 会把它 slug 成:
fix-login-bug
最后得到:
feature/alice-fix-login-bug
你不用在脑子里手动模拟这一遍。
CLI 当然也能直接用
scripts 是我推荐的团队用法,但 pumpp 本身也是一个普通 CLI。
你可以直接:
pumpp release
pumpp feature --desc login
pumpp hotfix --desc urgent-fix --push -y
也可以只预览,不动仓库:
pumpp release --dry-run
CI 里一般会配 -y 跳过确认。
- name: Create release branch
run: pnpm dlx pumpp-cli release -y --push
本地有人参与,保留确认;CI 没人参与,跳过交互。
这就是 pumpp 的基本策略。
团队自己的字段怎么办
很多团队的分支名不止 version、date、username。
可能还会有模块名、Jira ticket、需求 ID、环境名。
这种东西 pumpp 不可能内置完。
所以它有 tokenProviders。
比如你们希望:
feature/PROJ-1234-login
配置可以这么写:
import { definePumpConfig } from 'pumpp-cli'
export default definePumpConfig({
types: {
feature: {
pattern: 'feature/{ticket}-{desc}',
},
},
tokenProviders: [
{
name: 'ticket',
resolve: () => process.env.JIRA_TICKET?.toLowerCase(),
},
],
})
如果某个字段不适合自动解析,比如模块名想让用户现场输入,也可以标记成 interactive。
tokenProviders: [
{ name: 'module', interactive: true },
]
然后 pattern 写:
style: {
pattern: 'style({module})/{username}-{desc?}',
}
本地会问,CI 不会傻等。
如果必填 token 没值,pumpp 会直接失败,而不是偷偷生成一条半残分支。
这点我挺坚持。
因为 CI 里最可怕的不是失败,是带着错误信息成功。
后来我们怎么用
现在我更倾向于把 pumpp 当成「项目规范的一部分」。
不是让每个人记命令,而是让仓库告诉大家怎么创建分支。
比如新同学进项目,只需要看 package.json:
{
"scripts": {
"dev": "vite",
"test": "vitest",
"branch": "pumpp",
"branch:release": "pumpp release",
"branch:feature": "pumpp feature",
"branch:hotfix": "pumpp hotfix"
}
}
他会很自然地知道:
pnpm branch
就像他知道:
pnpm test
一样自然。
这就是我想要的效果。
不是让工具显得很厉害,而是让团队少记一件事。
最后想说的
分支名这个东西,看起来很小。
但它连接着很多后续流程:CI、发版、回滚、review、changelog、分支清理。
只要它是手敲的,就会飘。
只要它靠文本约束,就会被忘。
所以我的结论很简单:
团队规范不要只写在文档里,要尽量写进工具里。
文档适合解释为什么,工具负责保证怎么做。
pumpp 只是把这件事落到了「创建分支」这个很小的场景里。
如果你的团队也每周都要切发布分支,也在群里反复确认「这次 release 分支怎么命名」,或者你已经厌倦了每次手敲 git checkout -b,可以试一下。
pnpm add -D pumpp-cli
pumpp init
然后在 package.json 里加:
{
"scripts": {
"branch": "pumpp"
}
}
之后让团队从这一句开始:
pnpm branch
让机器记规范,让人少背一点。
谢谢大家👏,如果对你有帮助欢迎不吝点赞👍,也可安装 pumpp 使用下。