Documentation
¶
Overview ¶
Package mirror 提供"双仓双身份"的 Go module 镜像发布。
一份源码、两个可被 go get 的模块身份:canonical 仓库保持只读(连对象库 都不写入),每个 tag 在镜像侧合成为一个身份改写后的独立孤儿 commit (无父提交),各版本互不依赖,可对任意历史 tag 重跑。go get 按 tag 取 快照、proxy 按 tag 永久缓存,因此同 tag 的 tree 必须确定性一致——重跑 只允许改变 commit 包装,不允许改变内容;本包的合成 commit 继承源 commit 的作者/提交者身份与时间戳,同 tag 重跑 hash 完全一致。
流程:前置校验 → 读源树并全量身份改写 → 物化到临时目录跑 go build 门禁 → 镜像远端同名 tag 的 tree 一致性检查(不一致默认中止)→ 合成 tree/commit/附注 tag 写入临时仓库 → 强推 tag 与 main 快照 → 清理。
git 侧全部通过 go-git 对象 API 与 SDK gitbackend 完成,不执行任何 动态参数的子进程;go 工具链调用(build 门禁、模块验证)只用静态参数, 动态数据经由文件传递。
Index ¶
- func GofmtNormalize(path string, data []byte) []byte
- func HasMandatory(content []byte, rules []ScrubRule) bool
- func MaterializeTree(st storer.EncodedObjectStorer, treeHash plumbing.Hash, dir string) error
- func ReadModulePath(dir string) (string, error)
- func RewriteIdentities(data []byte, rules []IdentityRule) ([]byte, error)
- func TypeCheckGate(ctx context.Context, dir string) error
- func VerifyModule(ctx context.Context, module, version string) error
- type IdentityKind
- type IdentityRule
- type Mapping
- type Options
- type Report
- type Rewriter
- type ScrubHit
- type ScrubResult
- type ScrubRule
- type ScrubSeverity
- type ScrubSummary
- type TagReport
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func GofmtNormalize ¶ added in v0.13.0
GofmtNormalize 对 .go 文件做 gofmt 归一化(语义与 gofmt 一致,含 import 排序)。
**为什么必须归一化**:身份改写会改变 import 的字典序位置 —— 改写后的源内容会变成「未格式化」(实测 review-service:改写后 50 个文件违反 gofmt,两棵树原始都是 0)。不归一化就比对,每个引用了模块路径的文件都会留下 一处 1 行的假差异,40 个文件凭空变成「待人工确认」。
只处理 .go。语法错误时原样返回:这类问题由类型门禁(GOWORK=off 下的 全量类型检查)报错,不该在这里被吞掉。
func HasMandatory ¶ added in v0.13.0
HasMandatory 判断内容是否命中强制人工处理的规则。
func MaterializeTree ¶ added in v0.12.0
MaterializeTree 把 storer 中以 treeHash 为根的树写成目录内容, 供 go build 门禁编译。gitlink(子模块)跳过;symlink 原样重建。
导出以便历史同步模式复用:门禁必须跑在「按计划应用后的目标树」上, 而不是跑在源树上——差异判定、门禁、格式校验要针对同一份产物。
func ReadModulePath ¶
ReadModulePath 从 dir/go.mod 解析 module 行(兼容带引号写法),供壳层 创建通道时带出源模块身份。
func RewriteIdentities ¶ added in v0.12.0
func RewriteIdentities(data []byte, rules []IdentityRule) ([]byte, error)
RewriteIdentities 按 rules 对 data 做身份改写的一次性便捷入口。 需要对多个 blob 复用同一组规则时,请改用 NewRewriter。
func TypeCheckGate ¶ added in v0.12.0
TypeCheckGate 对 dir 下的 Go 模块做全量类型检查,作为推送前的编译门禁。
通过 golang.org/x/tools/go/packages 驱动 go 工具链完成语义加载与 go/types 检查(与 gopls 同一机制):无法解析的导入、缺失的包、语法与 类型错误都会在此暴露——这正是模块身份改写可能引入的全部错误类别 (纯文本替换不会产生代码生成/链接层的错误,后者本包不涉及)。 门禁不通过时调用方必须中止,不得推送。
这是唯一的编译门禁实现:身份镜像模式与历史同步共用。
func VerifyModule ¶
VerifyModule 验证 module@version 已可被 go 工具链正常消费,成功即同时 证明:仓库可达、版本存在、zip 拉取成功、模块 go.mod 声明的身份与 module 一致——与在临时模块中执行 `go list -m <module>@<version>` 等价。
实现:在临时目录写入仅含 require 的 go.mod(动态数据只经文件传递), 由 go/packages 驱动工具链解析该模块的全部包。需要网络与 GOPROXY 配置; 私有模块需按 go 惯例设置 GOPRIVATE 等环境。
Types ¶
type IdentityKind ¶ added in v0.12.0
type IdentityKind int
IdentityKind 身份规则的匹配语义,决定边界如何判定。
const ( // KindModule 按 Go module path 边界匹配:源标识之后必须跟非模块路径字符 // (字母/数字/下划线/点/连字符之外)或文本末尾。 // 子包路径会被正确改写,兄弟路径(b-extra / b.cool)不会被误伤。 KindModule IdentityKind = iota // KindHost 按主机名边界匹配:源标识之后必须跟非主机名字符 // (字母/数字/点/连字符之外)或文本末尾。 // 点被算作主机名字符,避免 git.enjoye.top.evil.com 被半改写。 KindHost // KindLiteral 按字面量词边界匹配:源标识的前后都必须是"非字母数字下划线" // 或文本端点。用于环境标识、部署代号一类的独立词。 KindLiteral )
func (IdentityKind) String ¶ added in v0.12.0
func (k IdentityKind) String() string
String 返回规则种类的可读名称,用于计划报告与错误信息。
type IdentityRule ¶ added in v0.12.0
type IdentityRule struct {
// Source 是待替换的源标识。
Source string
// Target 是替换后的目标标识。
// 为空表示"剥离":删除源标识本身,而不是替换成占位符——
// 占位符只是把内网痕迹换了个形式留在公开树里,反而更难发现。
Target string
// Kind 决定边界匹配语义,默认 KindModule。
Kind IdentityKind
}
IdentityRule 一条身份映射规则。
type Mapping ¶
type Mapping struct {
// Source 是 canonical module path,须与 RepoDir/go.mod 的 module 行一致。
Source string
// Target 是镜像 module path(如 github.com/org/repo)。
Target string
}
Mapping 描述一次模块身份改写:canonical module path → 镜像 module path。
type Options ¶
type Options struct {
// RepoDir 是 canonical 仓库的工作区路径(读取 go.mod、tag 与远端配置)。
RepoDir string
// Mapping 指定身份改写方向。
Mapping Mapping
// Tags 是要发布的 tag 列表,至少一个;每个 tag 独立生成孤儿快照,
// 支持批量补历史版本。单个失败即中止,之前成功的保留。
Tags []string
// Remote 是 canonical 仓库中指向镜像仓库的远端名,默认 "github"。
Remote string
// Auth 是访问镜像远端用的凭据;零值(AuthNone)时 native 后端以
// 本机 git 默认凭据(ssh-agent、credential helper)推送。
Auth gitbackend.AuthConfig
// ScrubRules 痕迹清理规则:发布前把内容里的内网痕迹清掉。
//
// **为什么镜像发布也需要它**:身份改写只管 **module path**,
// 而内容里还有别���内网标识 —— 内网 registry 地址、部署代号、令牌。
// 它们原样进**公开 module** 就是泄漏(历史同步侧早有这套规则)。
//
// 语义与历史同步侧**同一套原语**(mirror.ScrubRule):
// - auto:按 Replacement 替换(为空则删除命中内容);
// - mandatory:命中即**中止发布**(密钥类不进公开仓),不做自动替换。
ScrubRules []ScrubRule
// AllowOverwrite 允许覆盖镜像远端上 **tree 不一致**的同名 tag。
// false 时检测到不一致直接报错,保护已发布版本不被误覆盖。
//
// ⚠️ **覆盖 = 同一个公开版本号出现两个不同的 h1**:
// Go 的 checksum DB 对一个版本号只保留**首次**见到的 hash,
// 之后任何拿到新 zip 的消费者都会得到 checksum mismatch,
// 而且**无法通过重试自愈**(proxy 也会按版本永久缓存旧 zip)。
// 这是「对外可见的公开 module」上最不可逆的一类破坏 ——
// 所以它必须显式开启,且报告里必须打出「该版本号的 h1 将变化」。
AllowOverwrite bool
// AllowUnverifiedPush 远端不可达/认证失败时**盲推**(跳过一致性检查)。
//
// 与 AllowOverwrite 是两件事,必须分开授权:
// - AllowOverwrite:「我知道内容会变,确认覆盖」;
// - AllowUnverifiedPush:「我连不上远端,查不了状态,先推了再说」。
//
// 原实现里 AllowOverwrite 顺带允许了后者(文档没写),已拆开 ——
// 一个开关同时管「覆盖」与「盲推」,等于把两个风险捆在一起授权。
AllowUnverifiedPush bool
// DryRun 为 true 时执行到远端一致性检查为止(校验、身份改写、门禁、
// 远端比对),不合成 commit/tag、不推送;用于发布前预检。
DryRun bool
}
Options 是一次镜像发布的输入。
type Rewriter ¶ added in v0.12.0
type Rewriter struct {
// contains filtered or unexported fields
}
Rewriter 是编译后的身份改写器,可在多个 blob 上复用。
规则执行顺序按 Kind 固定为 KindModule → KindHost → KindLiteral, 同类规则保持声明顺序。这不是可选的约定:若先执行主机名规则, "git.enjoye.top/enjoydream/x" 这类模块路径的前缀会先被改掉, 模块规则随后找不到源标识,静默失效——路径改写看起来成功了, 实际上 import 全部指向了不存在的模块。
func NewRewriter ¶ added in v0.12.0
func NewRewriter(rules []IdentityRule) (*Rewriter, error)
NewRewriter 编译身份规则。规则非法时返回错误,不做静默降级。
func (*Rewriter) Pending ¶ added in v0.13.0
Pending 判断 data 里是否还有**本该被改写、却仍然原样留着**的源标识。
用来做残留检测:改写器能改的都改了,剩下的才是真正的残留 (比如二进制/超限 blob 走了「原样保留」路径,或者内容根本没过改写器)。
判据用与 apply 同一套边界语义:只有**满足边界条件**的源标识才算残留。 「app-extra」这种「共享前缀但不是同一个标识」不算 —— 它是另一个模块路径,不是我们这个身份漏改了。
func (*Rewriter) PublishForm ¶ added in v0.13.0
PublishForm 计算内容在**发布形态**下的样子:身份改写 + gofmt 归一化。
**两条链路共用这一个实现** —— 镜像发布(mirror.Publish)此前只改写、**不归一化**; 历史同步(mirror/history)改写后还要 gofmt。 于是同一份源码经两条链路发布出**不同的字节**: import 顺序会因 module path 改写而翻转,镜像侧没归一化 → 公开仓的 gofmt CI 挂、`go get` 用户拿到未格式化代码, 而且**类型门禁看不出来**(编译不看 import 顺序)。
func (*Rewriter) Rules ¶ added in v0.12.0
func (r *Rewriter) Rules() []IdentityRule
Rules 返回编译后的规则副本(按实际执行顺序),供报告与审计使用。
type ScrubHit ¶ added in v0.13.0
type ScrubHit struct {
// Rule 命中的规则名。
Rule string
// Category 命中的类别。
Category string
// Line 行号(1 起)。
Line int
// Text 命中所在行(截断到 200 字节)。
Text string
// Mandatory 该命中是否强制人工处理(密钥类)。
//
// 必须区分:自动清理类的命中**已随内容一起发布**,
// 强制人工类的命中意味着**这个文件没有被发布**。
// 不区分的话,审计报告会把两者混为一谈。
Mandatory bool
}
ScrubHit 一处痕迹命中。
type ScrubResult ¶ added in v0.13.0
type ScrubResult struct {
// Content 清理后的内容。
Content []byte
// Hits 全部命中项(含已清理与强制 hold 的)。
Hits []ScrubHit
// MandatoryHits 强制人工处理的命中项。
MandatoryHits []ScrubHit
// Cleaned 是否发生过自动清理。
Cleaned bool
}
ScrubResult 是痕迹清理的结果。
func ApplyScrub ¶ added in v0.13.0
func ApplyScrub(content []byte, rules []ScrubRule) ScrubResult
ApplyScrub 对内容做痕迹清理。
规则语义:
- ScrubAuto:按 Replacement 替换;Replacement 为空则删除命中内容;
- ScrubMandatory:命中即报告,但**不改动内容** —— 密钥类不自动处理。
清理必须可审计:每个命中都带行号与原文,出现在计划里。
type ScrubRule ¶ added in v0.13.0
type ScrubRule struct {
// Name 规则名,出现在计划的命中原因里。
Name string
// Category 类别分组(如 "internal-host" / "secret"),仅用于展示。
Category string
// Pattern 匹配模式。对**改写后**的内容生效。
Pattern *regexp.Regexp
// Severity 处理强度,默认 ScrubAuto。
Severity ScrubSeverity
// Replacement 命中后的替换文本。ScrubAuto 且为空表示**删除**命中内容。
Replacement string
}
ScrubRule 一条痕迹清理规则。
func StripInternalHost ¶ added in v0.13.0
stripInternalHost 是一条常用的自动清理规则:删除内网主机名。
用途:公开树不该出现内部 registry 地址。删除而不是替换成占位符—— 占位符只是把内网痕迹换个形式留在公开树里,反而更难发现。
type ScrubSeverity ¶ added in v0.13.0
type ScrubSeverity string
ScrubSeverity 是痕迹的处理强度。
const ( // ScrubAuto 可自动清理:命中后按规则的 Replacement 替换(或删除)。 ScrubAuto ScrubSeverity = "auto" // ScrubMandatory 强制 hold:命中即必须人工处理,**不做自动替换**。 // // 密钥类走这条:自动替换密钥可能留下半吊子状态,比留痕更危险。 ScrubMandatory ScrubSeverity = "mandatory" )
type ScrubSummary ¶ added in v0.13.0
type ScrubSummary struct {
// ByRule 各规则命中数。
ByRule map[string]int
// Total 总命中数。
Total int
// Mandatory 强制人工处理的命中数。
Mandatory int
}
ScrubSummary 汇总一组规则的命中情况。
func SummarizeScrub ¶ added in v0.13.0
func SummarizeScrub(hits []ScrubHit, rules []ScrubRule) ScrubSummary
SummarizeScrub 汇总一组规则的命中情况。
type TagReport ¶
type TagReport struct {
Tag string
SourceCommit string
Tree string
Commit string
// ReplacedFiles 是**内容真的被身份改写**的文件(仓库内相对路径)。
ReplacedFiles []string
// ResidualFiles 是**仍带着内网标识**的文件:规则本该改写,却因为
// 二进制/超限而原样保留。非空即意味着本次发布被中止(未推送)。
ResidualFiles []string
// VerifyCmd 是发布后建议执行的验证命令。
VerifyCmd string
// RemoteTagExisted 表示推送前镜像远端已存在同名 tag。
RemoteTagExisted bool
// TreeMatchedRemote 表示远端已有 tag 的 tree 与本次合成结果一致
// (重跑幂等的直接证据);仅在 RemoteTagExisted 且成功比对时有意义。
TreeMatchedRemote bool
// OverwroteDivergent 表示在 tree 不一致且 AllowOverwrite 下完成了覆盖。
OverwroteDivergent bool
// ScrubHits 本次发布清理掉的痕迹命中(逐条带规则名与行号)。
//
// 密钥类(mandatory)的命中**不回显原文**,只标记位置。
ScrubHits []ScrubHit
// TagAlreadyPublished 表示远端已有**同内容**的同名 tag,本次未重推。
//
// 幂等重跑不该改写对外可见的 tag 对象:tag 的 hash 变了,
// 已经 pin 了 pseudo-version(形如 v0.0.0-<时间>-<commit12>)的消费者
// 就拉不到那个版本了。内容一致时**什么都不动**才是真幂等。
TagAlreadyPublished bool
// H1WillChange 表示本次发布**改写了已发布版本的内容**:
// 同一个公开版本号将出现两个不同的 h1,已发布过该版本的消费者
// 会 checksum mismatch 且无法自愈。
//
// 报告里必须显式带出这一条 —— 覆盖是唯一会造成这种不可逆后果的动作。
H1WillChange bool
// Warning 面向人的提醒(覆盖/盲推这类「用户可能没意识到的后果」)。
Warning string
}
TagReport 是单个 tag 的发布报告。