Help us shape the future of Go! Take the 2026 Go Developer Survey

mirror

package
v0.16.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 24, 2026 License: MIT Imports: 25 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func GofmtNormalize added in v0.13.0

func GofmtNormalize(path string, data []byte) []byte

GofmtNormalize 对 .go 文件做 gofmt 归一化(语义与 gofmt 一致,含 import 排序)。

**为什么必须归一化**:身份改写会改变 import 的字典序位置 —— 改写后的源内容会变成「未格式化」(实测 review-service:改写后 50 个文件违反 gofmt,两棵树原始都是 0)。不归一化就比对,每个引用了模块路径的文件都会留下 一处 1 行的假差异,40 个文件凭空变成「待人工确认」。

只处理 .go。语法错误时原样返回:这类问题由类型门禁(GOWORK=off 下的 全量类型检查)报错,不该在这里被吞掉。

func HasMandatory added in v0.13.0

func HasMandatory(content []byte, rules []ScrubRule) bool

HasMandatory 判断内容是否命中强制人工处理的规则。

func MaterializeTree added in v0.12.0

func MaterializeTree(st storer.EncodedObjectStorer, treeHash plumbing.Hash, dir string) error

MaterializeTree 把 storer 中以 treeHash 为根的树写成目录内容, 供 go build 门禁编译。gitlink(子模块)跳过;symlink 原样重建。

导出以便历史同步模式复用:门禁必须跑在「按计划应用后的目标树」上, 而不是跑在源树上——差异判定、门禁、格式校验要针对同一份产物。

func ReadModulePath

func ReadModulePath(dir string) (string, error)

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

func TypeCheckGate(ctx context.Context, dir string) error

TypeCheckGate 对 dir 下的 Go 模块做全量类型检查,作为推送前的编译门禁。

通过 golang.org/x/tools/go/packages 驱动 go 工具链完成语义加载与 go/types 检查(与 gopls 同一机制):无法解析的导入、缺失的包、语法与 类型错误都会在此暴露——这正是模块身份改写可能引入的全部错误类别 (纯文本替换不会产生代码生成/链接层的错误,后者本包不涉及)。 门禁不通过时调用方必须中止,不得推送。

这是唯一的编译门禁实现:身份镜像模式与历史同步共用。

func VerifyModule

func VerifyModule(ctx context.Context, module, version string) error

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 Report

type Report struct {
	Tags []TagReport
}

Report 是整次发布的报告。

func Publish

func Publish(ctx context.Context, opts Options) (*Report, error)

Publish 按 Options 逐 tag 发布镜像快照。中止于首个失败的 tag, 此时返回的 Report 仍包含已成功部分,便于核对。

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

func (r *Rewriter) Pending(data []byte) bool

Pending 判断 data 里是否还有**本该被改写、却仍然原样留着**的源标识。

用来做残留检测:改写器能改的都改了,剩下的才是真正的残留 (比如二进制/超限 blob 走了「原样保留」路径,或者内容根本没过改写器)。

判据用与 apply 同一套边界语义:只有**满足边界条件**的源标识才算残留。 「app-extra」这种「共享前缀但不是同一个标识」不算 —— 它是另一个模块路径,不是我们这个身份漏改了。

func (*Rewriter) PublishForm added in v0.13.0

func (r *Rewriter) PublishForm(path string, data []byte) []byte

PublishForm 计算内容在**发布形态**下的样子:身份改写 + gofmt 归一化。

**两条链路共用这一个实现** —— 镜像发布(mirror.Publish)此前只改写、**不归一化**; 历史同步(mirror/history)改写后还要 gofmt。 于是同一份源码经两条链路发布出**不同的字节**: import 顺序会因 module path 改写而翻转,镜像侧没归一化 → 公开仓的 gofmt CI 挂、`go get` 用户拿到未格式化代码, 而且**类型门禁看不出来**(编译不看 import 顺序)。

func (*Rewriter) Rewrite added in v0.12.0

func (r *Rewriter) Rewrite(data []byte) []byte

Rewrite 对 data 依次应用全部规则。没有任何规则命中时返回原切片。

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

func StripInternalHost(host string) ScrubRule

stripInternalHost 是一条常用的自动清理规则:删除内网主机名。

用途:公开树不该出现内部 registry 地址。删除而不是替换成占位符—— 占位符只是把内网痕迹换个形式留在公开树里,反而更难发现。

func (ScrubRule) Mandatory added in v0.13.0

func (r ScrubRule) Mandatory() bool

Mandatory 判断该规则是否强制人工处理。

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 的发布报告。

Directories

Path Synopsis
Package history 实现「历史同步」:把内部源仓与公开目标仓之间的**内容差异** 整理成一份可审阅的同步计划,同时保留目标仓自己的提交历史。
Package history 实现「历史同步」:把内部源仓与公开目标仓之间的**内容差异** 整理成一份可审阅的同步计划,同时保留目标仓自己的提交历史。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL