最近一段时间,我在频繁修改一批技能。它们有的要补规则,有的要换入口,有的只是把一次失败后的处理办法写得更清楚。修改本身并不稀奇,真正让我停下来的是:这些内容已经值得用版本管理保护起来了,但如果每一次改动都原样留下,版本库很快也会变成另一种垃圾堆。
我当时提出的要求很简单:最好能用 Git,出问题时可以退回去,但不用保留太多版本。这个要求看似只是设置一个数量,实际却牵涉 3 个边界:哪些目录应该各自管理,哪些文件值得进入历史,以及旧版本到底怎样退出。版本管理的目标不是把所有过去都收藏起来,而是在可回退和可维护之间保留一条足够可靠的路径。
先把“全部版本化”这个念头放下
最容易出现的错误捷径,是看到一批技能放在同一个根目录,就直接给整个根目录建一个总仓库。这样做开始时很省事:只需要初始化一次,所有文件都能被看见,提交也像是在给整个系统做快照。
但我很快意识到,这个目录不只是文本。里面还可能有模型、媒体、日志、草稿和各种过程产物。它们的共同特点是体积大、变化频繁,或者本来就不适合用文本版本差异来表达。如果把它们一起纳入历史,第一次提交就可能把大量无关数据带进去,之后每次提交还会继续增加扫描和存储成本。
如果不处理这个边界,后果不是“仓库稍微大一点”这么简单。初始化会变慢,提交会变得不透明,真正需要回看的规则文件反而被大量二进制内容淹没;等到磁盘空间明显下降时,再想清理已经不只是删几个临时文件,而是要重新判断哪些历史可以动。
所以,表面上我是在问“Git 要不要保留很多版本”,实际上我是在问“版本库应该对什么负责”。它应该负责技能的可读、可审查、可回退部分,而不是替所有生成过的材料做永久仓储。
仓库边界决定历史会不会互相污染
接下来要决定的是,一个根目录建一个仓库,还是每个技能单独管理。一开始,集中管理似乎更容易统一执行;但技能之间并不是一个不可分割的项目。它们有自己的修改节奏、自己的规则文件,也可能由不同的工作流使用。
我最终选择每个技能目录独立建库,是因为回退动作必须有清晰的责任边界。某个技能最近一次改动出了问题,我应该能只查看和恢复它自己的历史,而不必在一棵巨大的提交树里区分其他技能的变化。仓库边界越接近真实的变更边界,回退就越像一个局部动作,而不是一次全局冒险。
这个选择也减少了互相污染。一个技能增加了规则,不应让另一个完全无关的技能在同一次提交里留下痕迹;一个目录里的大文件变多,也不应拖着其他目录一起承受。独立仓库并不是为了增加管理对象,而是为了让“这次改动影响谁”这件事可以直接从目录结构里读出来。
这里有一个容易忽略的好处:以后即使某个技能不再频繁变化,也可以单独暂停它的提交策略,不必修改一套覆盖所有技能的总配置。历史管理的粒度与变化的粒度一致,维护才不会被迫扩大。
文件准入比提交频率更重要
确定仓库边界以后,我没有马上批量初始化,而是先把文件准入规则说清楚。我的判断很朴素:文本规则、配置和必要的说明值得进入版本库;媒体、模型、日志、草稿和超过体积阈值的文件,应当挡在外面。
这不是因为大文件一定没有价值,而是因为它们的价值不适合放在这套轻量历史里。模型和媒体通常需要专门的存储方式,日志是运行过程,草稿是中间状态。把它们混进代码式版本管理,既不能获得好的差异阅读,也会让每次提交的意图变得模糊。
我给准入加上体积门槛,是因为文件类型判断并不总是可靠。一个看起来像文本的文件,也可能因为意外写入大量内容而拖大仓库。体积门槛相当于最后一道保险:即使前面的分类没有挡住它,也不让单个异常文件轻易改变整个版本库的成本结构。
当时还有一个实际问题:扫描大文件时,有些路径会遇到权限限制。严格模式下,一次扫描失败可能直接让整批初始化中止。我的处理不是把所有错误都忽略,而是让这类扫描降级:能确认的文件继续判断,无法读取的部分记录为未完成检查,不让一个权限异常摧毁整个初始化流程。质量门不是要求环境完美,而是要在不确定出现时,把风险限制在可解释、可补查的范围内。
保留数量必须是滚动上限
文件准入解决了“什么进入历史”,但没有解决“历史保留多久”。如果每次提交都保留,哪怕每个版本只有少量文本,频繁修改后也会逐渐累积。于是我把保留数量定义成明确的滚动上限,而不是一个模糊的“以后有空再清理”。
滚动上限的意义在于,它把清理从临时决定变成了正常流程。每次提交后,只检查当前历史是否超过规定数量;没有超过,就保持不动;超过了,就把最早的一段历史收敛为一个基线,再删除更早的版本。这样最新工作树不受影响,最近一段可回退历史仍然在,磁盘也不会随着时间无限增长。
这里最容易误判的是“压缩”这个词。它听起来像把旧历史藏到一个更小的文件里,似乎未来还能逐条翻看。实际效果更接近删除:基线保留的是某个时点的内容快照,基线之前的逐次变化不再保留。这个差别必须说清楚,否则使用者会以为所有历史仍然可以恢复。
我没有把保留数设成无限大,是因为这批技能的主要风险发生在最近的修改周期内。问题通常会比较快暴露,最近若干次提交足以帮助定位和回退;更久以前的每一个中间版本,未必还值得用持续的磁盘成本保存。保留历史不是越多越安全,真正有用的是覆盖当前风险窗口,并且让这个窗口可以预测。
先用临时仓库验证再批量执行
这类操作最怕“逻辑看起来对,批量跑起来才发现会破坏内容”。因此我没有直接把裁剪规则用于所有目标,而是先用临时目录模拟多次提交。测试的重点不是 Git 命令能不能执行,而是超过上限以后究竟剩下什么。
验证结果是,连续提交超过保留数后,历史数量收敛到设定值,最老部分变成一个基线,最新工作树内容没有改变。这个结果回答了两个不同的问题:历史确实被裁剪了,以及裁剪没有误伤当前正在使用的文件。只有两个问题都成立,滚动保留才算完成。
之后的批量初始化也暴露出一个值得保留的边界案例。某个体积很大的技能目录在扫描时遇到权限问题,第一次流程因此失败。定位后发现,失败不来自 Git 初始化,也不来自文件准入本身,而是扫描器把一个可降级的检查当成了致命错误。调整为非致命后,目录可以继续初始化,同时仍然把大文件挡在版本库外面。
这件事让我重新意识到,自动化流程的可靠性不只取决于正常路径。真正影响批量任务的,往往是一个目录读不了、一个文件类型判断不完整,或者一个异常没有被分级处理。把失败拆成“必须停止”和“可以记录后继续”,比单纯追求一次全绿更接近真实工作环境。
这套方法怎样复用到别的资料
后来我把这套做法概括成 4 个连续判断。先按变更边界拆分管理单元,让一次回退不会意外牵连无关内容;再按内容性质和体积做准入,只让可读、可审查、值得回退的文件进入历史;提交后执行固定数量的滚动保留,把磁盘成本变成可预测的上限;最后用临时副本验证裁剪前后内容一致,并为权限等异常准备降级路径。
这 4 步并不只适用于技能目录。任何频繁修改、同时混有文本和产物的工作区,都可以先问同样的问题:真正需要回看的是什么?哪些文件只是生成结果?一次恢复应该影响多大范围?历史成本有没有一个能被自动执行的边界?
如果答案都没有,版本管理很容易滑向两个极端:要么为了省事全部纳入,最后仓库变重;要么为了省空间几乎不留历史,出问题时只能凭记忆重做。方法的价值,就是把“留多少”拆成可解释的选择,而不是靠感觉来回摆动。
下次我会怎么做
下次遇到一批频繁变化的技能或资料,我会先画出目录边界,再看文件构成,最后才决定版本工具和保留数量。我不会先问“能不能全部提交”,而会先问“出了问题,我需要恢复哪一小块内容”。这个问题一旦清楚,仓库应该建在哪里,通常也就清楚了。
我会把文本白名单、体积门槛和排除项写成初始化前的检查,而不是等仓库变大以后再补救。对无法读取的路径,我会区分权限异常和真正的数据错误:前者允许记录并继续,后者才阻止流程。提交完成后,我会固定执行滚动裁剪,并用临时副本确认最新内容没有变化。
更重要的是,我会把基线当成一种有损的历史压缩来理解。它保留的是继续工作的起点,不承诺保存每一个更早的过程。如果某些版本确实有长期审计价值,就不应混在这套轻量仓库里,而要另行归档。一个成熟的版本策略,不是让过去全部留下,而是提前决定哪些过去值得为未来付费。
这次处理让我看到,版本管理最后管理的不是文件,而是注意力和风险。留下最近可用的判断,挡住不该进入的体积,把旧历史收敛成可解释的基线,下一次遇到问题时,我面对的就不再是一座仓库,而是一条仍然走得回去的路。