1. 一份没有历史的代码,为什么一开源就自动进入了嫌疑名单
1.1 开源 24 小时,技术圈的第一动作不是跑 demo,而是翻 git log
我这两年养成了一个习惯:看到一个想用的工具开源,第一件事不是克隆到本地跑 demo,而是先打开它的 commit 列表,看一眼它的“出身”。这个习惯在 ZCode 开源的那 24 小时里,成了很多人判断“有没有偷代码”的核心依据。
为什么大家要这么干?因为对开发者来说,git 仓库里的 commit 历史,就是一份账本。账本里记着每一笔代码是谁在什么时间提交的、为了什么事情提交的、中间经历了多少轮重构和 bug 修复。有了这本国账,外人至少能从时间线、作者、提交信息、分支合并轨迹里,大致还原一个项目是怎么长大的。
如果一份代码仓库只有一个 commit,打开一看全是“init project”或者“release v0.1”,那问题就来了:这堆代码是从哪来的?是作者一行行写出来的,还是把现有某款工具改名换姓重新提交的?在没有历史可以参考的时候,不确定性本身就等于嫌疑。这不是针对 ZCode 一家,而是所有名人开源项目都会经历的考验。
我当时还专门去模拟了一下其他开发者的视角:先把仓库 clone 下来,执行git log --oneline --all --graph,发现历史干净得像刚办完注销手续的老公司;再执行git ls-files,看目录结构和常见编码助手项目是不是高度重合;最后再去翻依赖清单,看有没有“改头换面”保留原样的包名。这三步走完,舆论基本已经形成了。
1.2 “干干净净”的单版本仓库,反而让归属问题无解
很多人不明白,为什么我挺反感“只要开源就必须全网跪着夸”这种情绪。原因很简单:开源社区对“历史”这件事的敏感,是血泪教训换来的。
以前就有过不少案例:某个厂商把知名开源项目拉下来,把包名、命名空间、一些核心注释改掉,然后压成一次提交,放进自己的组织仓库里重新开源。代码确实能用,License 也确实写上了,但明眼人一看就知道底子是别人的。可问题是,当仓库只有单一次提交时,你很难通过 git 层面去证明它是从哪个项目演进出来的。没有中间 commit、没有原始作者的署名痕迹、没有 fork 关系,这时候只看仓库本身,你能拿出的证据只剩下代码内容相似度对比。
也就是说,“一份没有历史的账本”回答不了“有没有偷代码”这个灵魂拷问。账本不是拿来记功劳的,它是拿来记录来源、授权和演进的。你把原来的账页全撕了,只给新公司挂一块崭新的招牌,别人当然有理由怀疑你进货渠道不透明。哪怕你确实是完全原创,背后有辛辛苦苦的开发过程,这个沟通成本也已经付出了。
所以我看 ZCode 这个讨论时,最关注的其实不是“它到底偷没偷”,而是:一个项目开源时该不该把历史整理清楚?答案很明确,应该。可惜很多人把开源理解成“把代码的 tar 包公开”,忽略了开源首先是一个透明的协作协议,而 git 历史就是透明性的基础设施。
2. “有没有偷代码”:一个技术问题,为什么偏偏不好回答
2.1 git 账本能证明的部分和证明不了的部分
先摆一个硬核事实:git 历史再好,也不能百分之百证明“这段代码是原创的”。git 只能证明,这个仓库里的文件在某个时刻点被某个提交者推上来过一次;它无法证明这个人在推上来之前,屏幕背后有没有开着另一个项目做参考。
但 git 历史能提供的东西,价值非常大:它能证明项目的演进过程是有连续性的。比如说,一个仓库有 3000 次 commit,从最简单的 hello world 一步步变成完整产品,每次提交都在改接口、补测试、修样式,这些痕迹非常难以伪造,因为你要伪造就得连 bug 演进过程、发版时间、作者行文习惯一起仿真出来。相比之下,单次提交发布一个成熟产品,等于把“进化过程”全删了,只给观众看一个成年人的照片。你说你成年了,我们相信,但身份证上的户籍信息也得让人查一查吧。
这背后其实是开源信任模型的变化。以前开源是一个“先看代码再决定信任”的世界,只要你把源码交出来,哪怕没有历史,大家也能自己编译、审查、验证。但现在大量开源项目牵扯到 AI 代码生成、商业化授权、大公司竞争,公众对代码来源的敏感度提高了。单次提交、无历史、无演进记录的大型项目,天然会激发一种“来吧,让我看看你还能怎么解释”的审查心理。
所以我说,git 账本虽然不能回答“这是不是偷的”,但它能回答一个更前置的问题:这个项目是否具备“可追溯的成长过程”。可追溯性一旦缺失,项目就只能靠代码质量本身自证清白,而代码相似度又是一把双刃剑——大家都写快速排序,凭什么说是我抄的你?
2.2 真正做代码同源审计时会看的那几个硬信号
很多人以为判断代码是不是抄的,就是把两份代码放一起跑一下diff,看重复率。实际操作中,同源审计比这复杂得多,我一般分四个维度去看。
第一是结构指纹。diff只能看到文本层面是否有相同行,但真正比较可靠的是抽象语法树。同一段逻辑,别人可以换掉变量名、调整空格、把 if 改成 switch,文本相似度可能降到 20%,但 AST 结构可能依然高度一致。如果用工具把两份源码分别解析成 AST,再对树形结构做相似度比对,往往能发现“换皮”痕迹。
第二是依赖关系。一个项目如果把另一个项目的requirements.txt或package.json原样搬过来,哪怕代码重写了,依赖树也会暴露底子是哪个生态的。尤其是一些冷门的、带特定版本锁定的依赖组合,重复本身就是一种信号。
第三是注释、字符串常量和错误信息。代码逻辑可以重写,但注释里的口癖、硬编码字符串里的特殊文案、异常信息里的英文措辞往往会在重构时被保留下来。这些看似不重要的碎片,反而是抄代码时最容易被忽略的部分。
第四是文件组织习惯。目录叫core、modules、utils是大多数项目的通用习惯,但一些项目会有独特的目录切线,比如把业务模块放在src/features下按领域划分,或者喜欢用lib/internal这种比较少见的布局。如果两份源代码的目录结构呈非常高的匹配度,又都没有历史,那被质疑就很合理了。
2.3 相似度与“偷”之间,还隔着一条举证链
我之所以反复强调“历史无法直接证明偷没偷”,是因为代码相似度高,并不当然等于“偷”。在开源世界里,同源、借鉴、改写、碰巧撞车,都是真实存在的情况。
举一个最直白的场景:一个新开发的终端工具和一个老牌终端工具,界面长得像、命令名称像、配置文件格式也像。它可以是因为“大家都在遵循同一套行业惯例”,也可以是因为“开发者就是照着老工具的手感设计的新工具”。在没有 commit 历史、没有设计文档、没有 roadmap 的情况下,你无法从代码本身区分这两种情况。于是大家就会退回去看一个更现实的问题:这个开源项目到底有没有提供足够的“出身说明”?
这也是为什么我对 ZCode 这种名场面的态度不是“快下载代码找证据”,而是“看它的仓库元数据和组织说明”。真正负责任的团队,会在 README 里写清楚技术选型依据、核心架构来源、第三方代码引入清单。哪怕历史被压平了,这些文字也可以充当账本的补页,告诉外人“我这个项目是从哪条路上走的、有没有跟别人共享过一段路程”。
反过来,如果这些信息都没有,只剩一个孤零零的 release commit,那舆论失控几乎是必然的。因为在公众眼里,你不提供账本,账本里的空白区域就会被猜测填满。
3. 给开源项目留下一本能查的账:仓库应该有的最低限度
3.1 合规开源最容易被忽略的不是 License,而是“可解释的出处”
现在大部分开发者都知道开源项目要加 License,但对“出处可解释”这件事完全没有概念。License 解决的是“别人能用”,而“可解释的出处”解决的是“别人凭什么相信你没偷”。
我见过非常多的商业项目转开源,第一版仓库都是压成单次提交,代码是能跑的,License 也加了,但一问架构师:这个设计参考的是哪个项目?哪些模块是从旧项目里重构成过来的?为什么有这么高的相似度?答不上来。
这非常危险。开源一旦遭到社区质疑,第一个动作一定是去翻历史,而不是读你的 README。如果历史不存在,第二动作就是去搜相似项目做 diff。你当年参考了别人的设计、用了同样的依赖、拉了别人的代码分支,这些事本来是可以大大方方写出来的,只要标注清楚来源,反而能获得比“假干净”更高的信任。
所以我现在带团队做开源发布,一定要求仓库至少包含三类出处信息。第一是 README 里的技术渊源说明,明确写“本项目的 X 模块参考了 Y 项目的实现方式,并按 Z 许可证使用”。第二是 THIRD_PARTY_NOTICE,把所有涉及第三方代码的文件和许可证列出清单。第三是提交信息里的链接引用,比如某次提交引用了某个 issue、某篇论文或者某个原始仓库的 URL。这三样东西不需要长,但必须真实存在。
3.2 真要压历史或单提交发布时,该怎么补“出处说明”
有时候压历史不是想偷懒,而是有实实在在的原因:公司不允许把内部敏感提交暴露出来;或者历史里包含了客户名、密钥、内部邮箱;又或者项目经历过多次大规模重构,旧提交已经完全不能反映当前结构。这些情况下,单次提交开源是可以理解的。
但要补课。我建议在仓库里加一份HISTORY.md,用普通文档把“账本丢失”的原因和项目演化过程交代清楚。它不需要写具体代码,只需要按时间线说明项目的几个关键节点:什么时候立项、什么时候决定转型开源、中间有没有大版本重写、哪些数据被清理了、为什么清理。
这个文件的价值在于“主动交代”。社区不是不能接受压缩历史,而是不能接受“你什么都不说,假装自己从来都是这个样子的”。你主动写一份 history,告诉大家大版本重写导致旧提交不复存在,质疑声就会迅速缓和。你什么都不写,那大家只能靠猜,猜到最后往往是最坏的结论。
如果项目是从别的开源仓库 fork 出来的,这一点上更是绝对不能含糊。git 里有一种正规做法叫“保留 fork 关系”:从原项目 clone 下来之后,后续的 commit 全部叠在原始历史上,官方会看到你的 fork 来源;如果你的改动方向跟原项目不一致,想重新开始,也应该在 README 里写明“本仓库基于 xxx 的某个版本 fork,此后独立维护”。这是开源社区的基本礼貌。
3.3 一个可用于“交付即上线”的开源发布清单
我整理过一份自己的开源发布清单,在写完标题里那种“单提交仓库”之后,至少要对着走一遍。分享出来给大家参考:
- 代码层面:清理硬编码密钥、内网 IP、客户专属配置;移除无用的
/vendor、/node_modules提交。 - 历史处理:如果保留真实历史,确认没有泄露隐私;如果压缩历史,补写
HISTORY.md说明原因和项目里程碑。 - License 层面:明确主许可证;列出所有第三方依赖的许可证;对包含版权的资源文件单独标注。
- 出处说明:在 README 中新增“Thanks / 来源说明”章节;若是在其他项目基础上开发,写明 fork 来源与版本。
- 发布佐证:为 release 版本打好 tag,并给核心提交做 GPG 签名,方便别人验证提交人身份。
- 响应预案:提前预判最可能的 3 个质疑点,准备好对应的证据或说明文档。
这里面最容易被忽略但最关键的一点是“响应预案”。开源发布不是把代码推上去就结束了,至少要准备一页纸来解释:为什么这份代码长这样?它的来源是什么?如果需要改历史,理由是什么?不要等别人问到你脸上再开始整理,到时候手忙脚乱,只会让质疑者觉得你心虚。
4. 被别人质疑抄代码,以及我去审别人的仓库,都需要什么样的证据链
4.1 被质疑后,我建议按这个顺序整理证据,而不是急着发公告
如果你维护的项目不幸成了“开源 24 小时内被全网审判”的主角,第一反应千万别是写一条“这是我们的原创”的声明,那样没有任何说服力。更好的做法是,按照下面几个顺序去把证据链搭起来。
先整理时间线证据。找到项目最早的立项文档、需求记录、内部 demo 录屏、任务看板截图,把它们按时间排好。这些东西能直观地证明你在某个时间点已经启动开发,而不是等到别人开源之后才“参考”。
再整理仓库演进证据。如果你的仓库已经压成了单提交,那仓库本身没救,但你可能还有本地的完整历史。把这部分的git log导出来,哪怕不公开到线上,也可以提供给可信的第三方或社区代表核查。历史里如果有大量中间提交,还带着日常修复、接口调整、重构记录,那可比一句“原创”公告有说服力得多。
接下来整理开发过程佐证。这里指的不是聊天记录截图,而是能够客观留存的东西:CI 构建日志、版本发布记录、测试覆盖率报告、文档更新时间。比如你的 CI 日志显示半年前就开始持续构建了,而对方项目是三个月前才开源的,光靠时间线就能推翻很大一部分“抄代码”指控。
最后才是代码级的说明。如果确实参考了某个开源项目,就老老实实写清楚引用了哪些模块、用的什么许可证;如果某些模块碰巧相似度高,就解释清楚为什么——是行业惯例,是标准算法,还是同一个作者在不同项目间的代码转移。这个顺序的意义在于,先用时间线和过程证据建立信用,再回头解释相似度问题,公众才会听。
4.2 使用开源项目的“三查”习惯,避免连踩带盲
站在使用者的角度,我也有一份自己常年用的仓库审查清单。代码相似度审查未必是普通用户该做的事,但“三查”习惯人人可以做。
第一查许可证。确认它到底是什么协议,是 MIT、Apache 2.0、GPL,还是一个加了附加条款的自定义 License。注意很多“开源”项目只是公开了代码,但注明了“只允许查看、不允许商用”,那它就不算真正意义上的开源。落到生产环境,风险会翻倍。
第二查提交时间与活跃度。看最近一年是否还有 commit,看 issue 有没有人维护,看 release 有没有持续发版。如果项目是单次提交后就静默了,那不管代码多漂亮,都要小心:它可能没经过充分的社区测试,也可能发布方对自己代码都没自信。
第三查依赖与来源。打开依赖声明文件,搜一搜有没有一些冷门的包;再看 README 里是否写了来源说明。一个光明正大站在别人项目肩上的开源项目,通常会把来源写在很显眼的地方,而不是藏起来等用户问。来源信息不透明的大项目,我用之前都会多留一个心眼。
这三查不复杂,加起来不超过十分钟,但那十分钟能让你少踩很多坑。代码历史和许可证就是开源世界的“质检报告”,不做质检就把东西搬进生产环境,出了问题再去补,代价往往是翻倍的。
5. 我复盘这次事件后给自己定的几条铁规矩
5.1 一条提交历史也是一份无形资产
以前我总觉得 commit 历史是开发过程的副产品,只要能编译、能跑,commit 写成什么无所谓。经过这次对 ZCode 事件的复盘,我的想法变了:提交历史是项目最重要的无形资产之一。它不仅是开发者工作的日记,更是未来应对质疑的第一手证据。
所以我现在给自己定了一条铁规矩:哪怕是小项目,至少保证每个提交对应一个明确的逻辑单元;提交信息里写明“为什么”而不只是“改了什么”;每个发版打 tag;涉及外部代码的时刻,直接在提交信息里留下来源链接。这些动作的成本很低,但能在关键时刻替你挡掉最麻烦的“你的代码是不是抄的”这类诉讼式提问。
5.2 开源是能力,把开源的方式交代清楚是更高的能力
我见过太多团队把“开源”看成一场宣传行为:找一个合适的日子,把仓库公开出去,写一篇漂亮的发布公告,然后等着大家夸。他们忽视了开源社区真正的运行逻辑:代码会被人质疑,历史会被人翻查,License 会被人逐字核对,甚至提交者身份都会被人追踪。这不是无聊,这是开源社区长期形成的信任机制。
一个项目想要在社区里活得久,靠的从来不是“我有非常严苛的代码规范”或“我用了最先进的技术”,而是“我的每一步变化都能被查证”。把开源的方式交代清楚,把自己的出处说明白,允许别人通过历史来审视你,这才是一个项目真正成熟起来的标志。
这次 ZCode 的讨论还会持续,我也没有能力靠一个 git log 就判定它有没有“偷代码”。但有一点我可以确定:如果一个仓库从一开始就愿意给别人一本完整的历史账本,它至少不会被假的嫌疑贷走太多信任。