大多数 Git 教程都是为团队编写的。它们描述了至少需要两个人的拉取请求工作流,需要管理员配置的分支保护规则,以及假设有人在另一端等待的审查流程。如果你独自开发软件,你可能已经读过这些指南,点头认同,然后悄悄地忽略了大部分——因为在周二晚上11点独自发布代码的现实情况中,这些都不适用。
问题不在于独立开发者的 Git 工作流文档不足。问题在于它们几乎完全没有被文档化。大多数资源要么将版本控制视为团队协调问题,要么将其简化为”频繁提交并推送到主分支”。这两种极端都不适合运行一个或多个真实产品的独立开发者。
接下来要介绍的不是教科书式的方法。这是来自多年独立发布经验的方法——是实际做出的决定,遇到的陷阱,以及在独自开发特定压力下经得起考验的实践。
为什么 Gitflow 不适合单人项目
Gitflow 由 Vincent Driessen 在 2010 年设计,用于解决特定的协调问题:多个开发者同时开发多个功能,开发与生产之间有严格的发布边界。对于那个问题,它是一个很好的解决方案。但如果那个问题不是你的,它就是一个糟糕的解决方案。
对于独立开发者,Gitflow 引入了你永远不会按预期实际使用的分支。develop 分支变成了半成品工作积攒的地方,因为没有其他审查者控制什么可以进入。发布分支被创建并在同一会话中合并,因为”发布过程”只是你运行部署脚本。热修复分支在程序上看似正确但在逻辑上显得荒谬——你已经知道 bug 在哪里,你已经知道这很紧急,而从 main 分支创建分支、修复、合并到 main,然后再合并回 develop 的开销,让一个五分钟的修复变成了十五分钟。
GitHub Flow 更简单且更适合持续部署,但它仍然假设工作的基本单位是由其他人审查的功能分支。当你自己是审查者时,分支只是一个延迟。
对于独立工作,值得内化的思维模型不是”我如何与他人协调”,而是”我如何保护自己免受自己的影响”。这是两个不同的问题,需要不同的解决方案。
基于功能标志的主干开发:实用的默认选择
对于独立项目,最能经受考验的方法是直接提交到main分支,并通过功能标志控制实际生效的内容。这不是因为它很时髦,而是因为它符合实际的约束条件:你不会因为并行贡献者而产生集成延迟,而你的主要风险是部署损坏的代码,而不是合并冲突的功能工作。
功能标志不需要第三方服务。对于大多数独立项目,一个简单的环境变量或配置表中的一行就足够了。模式如下所示:
- 新功能在
main分支上发布,但在生产环境中将标志设置为false - 代码始终处于可部署状态—不完整的功能存在于代码库中,但用户无法访问
- 当功能准备就绪时,切换标志只需一行配置更改,而不是分支合并
- 回滚是切换标志回去,而不是提交回滚
这种方法消除了这样一类问题:你在分支中工作了两周,现在合并回主干变成了与分歧历史的协商。没有分歧的历史。你一直在向同一个分支提交,分歧存在于你的标志配置中,而不是你的git图中。
通常的反对意见是”如果未完成功能的代码很混乱怎么办?”答案是:它在分支上也会很混乱。功能标志方法只是迫使你在每次提交时保持代码库可编译和可部署,这实际上比分支开发更高的纪律要求—而且是有用的。
分支何时真正值得存在
排除Gitflow并不意味着完全排除分支。对于独立开发者来说,有三个场景分支确实是正确的工具,清楚地认识这些场景可以防止相反的失败:从不分支,当实验变得具有破坏性时没有恢复点。
结果不确定的真实实验。如果你正在尝试 fundamentally 不同的方法—重写数据模型、迁移ORM、尝试新的渲染策略—并且你真的不知道它是否会成功,那么分支是合适的。关键限定条件是”真的不知道”。如果你知道它会成功,你只是在实现它,那这就是一个功能,它应该放在主分支上的标志后面。实验是你可能需要完全放弃的东西,这意味着你需要一种干净的方式来做到这一点,而不需要在主分支上回滚十几个提交。
客户工作或多重部署目标。如果你维护一个为特定企业客户提供白标版本的 SaaS,或者你的产品部署到具有不同功能集的多个环境中,那么针对每个部署目标的长期分支是合理的。这不是 Gitflow——这些分支代表真正不同的部署状态,而不是协调检查点。管理它们需要谨慎处理哪些内容需要挑选合并,哪些需要保持隔离,但分支本身是有其合理性的。
你尚未准备好发布的破坏性更改。偶尔你开始重构并意识到更改比预期的更深,今天无法完成。分支可以让你暂停这项工作,而不会让主分支处于损坏的中间状态。这里的纪律是保持分支的生命周期短——几天,而不是几周。如果重构分支存活超过一周,它就变成了上下文切换的负担,而不是安全机制。
周末分支反模式
在独立开发者仓库中,有一种特定的失败模式定期出现,这表明它是结构性的,而非偶然的。我们称之为周末分支。
它开始于周五或周六下午。你有不间断的时间,清晰的头脑,和一个大胆的想法。你创建了一个分支——可能叫做 feature/new-dashboard 或 refactor/auth-overhaul——并花四到六个小时取得了实质性进展。然后周日结束,工作周开始,这个分支有九天没有被碰过。当你回到它时,你已经忘记了中间状态,主分支已经前进,合并现在确实变得复杂了。
周末分支反模式实际上并不关乎周末。它关乎独立项目中因错误原因创建的长期分支。开发者不是在”这可能行不通”的实验意义上使用分支,他们只是在一段心流状态中构建功能,并出于习惯或团队惯例而默认创建分支。
解决方案有两个方面。首先,对于你计划发布的功能,使用标志而不是分支——它们不会与主分支分离,当你回来时也不需要追赶。其次,如果你确实创建了一个分支,给它设定一个个人截止日期。在独立项目中,没有合并目标日期的分支就是会导致问题的分支。把它当作一个有时间限制的调查,而不是一个无期限的工作流。
无人审查时的提交信息纪律
撰写良好提交信息的最常见论点是,你的同事需要理解你的更改。去掉同事,许多独立开发者得出结论:提交信息不重要。这在短期内是正确的,但在中期代价高昂。
项目进行六个月后,你会通过二分法定位到一个名为 fix stuff 的提交。你将完全不知道”stuff”指的是什么,为什么采用那种修复方式,以及周围的代码是有意为之还是临时添加。你会花上三十分钟进行考古式挖掘,重构一个你当初只用了十分钟就做出的决定,而且只用两个词就描述完了。
适用于单人项目的标准比常规提交指南更轻量,但又比”fix stuff”更详细。值得采用的格式如下:
- 第一行:祈使动词,主题,不超过50个字符 — 提交做了什么
- 空行
- 正文:一到三句话解释为什么,而不是什么—什么内容在差异中,为什么不在
“为什么”才是关键。六个月后,”移除搜索输入的防抖功能”这句话对你没有任何帮助。而”移除搜索输入的防抖功能—移动用户报告了延迟;实时感觉更好,服务器可以在当前规模下处理负载”则告诉你做出的权衡以及可能需要重新审视的条件。
当你唯一的提交者时,强制执行这一纪律需要让编写好的提交消息比编写坏消息更容易。实用的工具是在全局 .gitconfig 中设置提交消息模板:
将
commit.template设置为一个包含你首选结构的文件—动词、主题、空行、为什么正文占位符。Git会在每次提交时在你的编辑器中打开它,仅凭视觉支架就能显著减少懒散消息的数量。
没有发布经理项目的标记和发布策略
单人项目往往持续发布,没有正式的发布事件,这导致仓库中的每个主分支提交名义上都是”生产环境”版本,但从未命名任何版本。这造成了一个特定问题:当出现问题时,如果不手动查看差异,你无法快速识别”上周还能工作”和”现在已损坏”之间发生了什么变化。
轻量级标记策略可以解决这个问题,而不需要发布流程。适用于单人持续部署的惯例如下:
- 在推送到生产环境前,用语义版本标记每个部署 —
v1.4.2 - 使用带注释的标记,而不是轻量级标记:
git tag -a v1.4.2 -m "添加支付重试逻辑;修复会话过期错误" - 在部署脚本中自动化标记推送,使其一致执行,而不是依赖记忆
带注释的标签是可搜索的,带有时间戳,并支持消息 — 这意味着你的部署历史变成了带有可读描述的发布时间线日志。结合合理的提交信息,这为你提供了足够的追溯能力,无需完整的变更日志系统即可调试回归问题。
即使没有外部 API 合同,也值得使用语义化版本控制。补丁/次要/主要的区分会迫使你为每个发布进行有意识的分类,这会在变更到达用户之前,而不是之后,偶尔让你产生”等等,这实际上是一个破坏性变更”的醒悟。
当你是唯一提交者时,Rebase 与 Merge 的选择
关于 rebase 与 merge 的争论通常被框定为关于历史整洁性的团队理念问题。对于独立开发者来说,答案更加机械化:几乎总是使用 rebase,只有在特定原因下才使用 merge。
当你是唯一的提交者时,merge 提交通常只是噪音。它代表了合并分支的行为,而这些分支的存在是因为你选择的分支模型,而不是因为实际的并行工作。在独立仓库中的 merge 提交看起来像是协调开销,却没有实际的协调。它们使 git log --oneline 更难阅读,并略微增加了二分查找的复杂性。
在合并之前(或只是快进)将你的功能分支 rebase 到 main 上,可以保持历史的线性。线性历史更容易导航,更容易进行二分查找,并能更清晰地传达代码库的演变过程 — 当你几个月后回顾自己的历史时,这一点很重要。
merge 在独立工作中明显正确的唯一情况:将一个长期存在的客户端分支合并回共享基础分支。这里的 merge 提交是有意义的 — 它代表了真正不同开发线之间的有意识集成事件,并且你希望这个事件在历史中可见。在这种情况下,--no-ff 是合适的,提交消息应该描述这次集成了什么。
黄金法则是:如果你会为向未来的自己展示 git log 而感到尴尬,那么在它到达 main 之前就修复它。交互式 rebase — git rebase -i</code — 是用于压缩修复提交、重新排序提交以获得逻辑清晰度,以及编辑那些命名草率的提交消息的工具。在分支接触 main 之前,可以在本地分支上自由使用它。
今天值得安装的实用 .gitconfig 别名
配置是独立 git 实践要么随时间退化要么产生积极累积效应的地方。从事这项工作多年的开发者往往拥有高度定制的 .gitconfig 文件,每天可以消除数十个微小摩擦。以下是最能持续发挥作用的别名:
lg: 一种格式化的日志,在一行中显示分支图、作者、日期和消息。标准的git log输出冗长且难以扫描。一个好的lg别名将历史记录变成你真正会查看的内容:log --oneline --graph --decorate --allundo:reset HEAD~1 --mixed— 撤销最后一次提交并保留更改到暂存区。当你提交过早或想在提交变为永久之前重构提交时很有用。wip:!git add -A && git commit -m "WIP"— 当你需要紧急切换上下文且没有时间写真实提交时的快速保存点。惯例是,在 WIP 提交到达主分支之前,总是修改或压缩它们。aliases:config --get-regexp alias— 列出所有配置的别名,因为每个独立开发者最终都会忘记自己设置了什么。ignored:ls-files --others --ignored --exclude-standard— 显示 gitignore 实际隐藏的所有内容,当你怀疑某个文件被静默排除时很有用。
除了别名,独立开发者经常忽略的两个全局设置:将 push.default 设置为 current,这样不带参数的 git push 会将当前分支推送到同名的远程仓库。并将 pull.rebase 设置为 true,这样从远程拉取时会自动变基而不是创建合并提交——在独立项目中,这几乎总是你真正想要的。
实际目标:六个月后仍能理解的代码库
独立项目中的每个 git 决策都回到同一个问题:当你稍后回到这段代码时,能否重构自己的推理过程?不是你的队友能否理解它——你没有队友。当上下文已经模糊,而你思维的唯一记录就是你的提交时,你还能理解它吗?
这里概述的做法不是为了遵循惯例而遵循。基于标志的主干开发始终保持代码库处于可部署状态,这是一种约束,可以防止一类自找的问题。良好的提交消息是写给未来自己的笔记。带注释的标签创建了一个你可以导航的时间线。变基保持历史线性且可读。这些都不难。它们会慢慢累积成一个真正的资产,而不是一堆未带日期的差异。
在独立项目中从版本控制中获得最大价值的开发者,不是那些严格执行命名分支策略的人。而是那些已经内化了每项实践存在的原因,并能根据实际情况适度应用这些实践的人。独立开发者不需要Gitflow的协调机制。他们需要一套不同的习惯——这些习惯是为了应对作为自己过去、现在和未来合作者的特定挑战而设计的。
从.gitconfig清理开始。修复下一个项目的提交信息。为下一次生产推送添加部署标签。小而一致的实践所产生的复合效应,优于你可以从与你约束不同的团队那里借鉴的任何分支模型。
