74ba737b1e
build / build (push) Successful in 1m4s
历史分支中存在两条开发号 0.1.1;归档选择按 SHA 匹配,保留原编号与冲突标签。 增加同号前置说明遮蔽及错 SHA 不复用回归,标签计划使用相同精确选择方式。 已运行:定向 Go 测试、快照工具临时仓正反例及整仓 make verify;未部署。
228 lines
7.2 KiB
Go
228 lines
7.2 KiB
Go
// Package versionhistory 把「远端 main 可达的提交」+「tag 证据」+「提交内 CHANGELOG」
|
|
// 换算成版本记录页要的提交记录。
|
|
//
|
|
// 它**只读、纯计算**:不发网络请求、不写库、不执行任何历史提交里的脚本。版本号口径
|
|
// 委托给 internal/governance.DecideVersion,提交数按提交图算(等价 `git rev-list --count <tag>..<sha>`),
|
|
// 与 scripts/version.py 同规则 —— 两处算出的号必须一致。
|
|
package versionhistory
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
)
|
|
|
|
// RawCommit 是一条从 Gitea 读回的提交(只保留展示与判版本所需的字段)。
|
|
type RawCommit struct {
|
|
SHA string
|
|
Parents []string
|
|
CommittedAt time.Time
|
|
AuthorDisplay string
|
|
Subject string
|
|
Body string
|
|
}
|
|
|
|
// RawTag 是一个 tag 及其可核验属性;Annotated=false 表示轻量 tag(不算正式版本的证据)。
|
|
type RawTag struct {
|
|
Message string
|
|
Name string
|
|
TagSHA string
|
|
TargetSHA string
|
|
Annotated bool
|
|
}
|
|
|
|
// ChangelogSource 读「某个提交内的 CHANGELOG.md」;读不到返回错误(调用方按"无记录"处理)。
|
|
type ChangelogSource func(sha string) (string, error)
|
|
|
|
// DeriveInput 是一次换算的全部输入。
|
|
type DeriveInput struct {
|
|
DeliveryArchive string
|
|
RepoKey string
|
|
CommitURLBase string // 形如 "<origin>/<owner>/<repo>/commit/",由配置拼好,不接受外部传入
|
|
TagURLBase string // 形如 "<origin>/<owner>/<repo>/src/tag/";空则不下发 tag 链接
|
|
Commits []RawCommit
|
|
Tags []RawTag
|
|
Changelog ChangelogSource
|
|
SnapshotID int64
|
|
}
|
|
|
|
// DeriveResult 是换算结果。
|
|
type DeriveResult struct {
|
|
Records []governance.VersionHistoryCommit
|
|
GraphComplete bool
|
|
MissingParents []string
|
|
}
|
|
|
|
// DeriveVersions 逐提交判定版本号(含合并提交与侧分支提交;不展示任何未推送的本地提交,
|
|
// 因为输入本身就是远端 main 可达集合)。
|
|
func DeriveVersions(in DeriveInput) DeriveResult {
|
|
bySHA := make(map[string]RawCommit, len(in.Commits))
|
|
for _, commit := range in.Commits {
|
|
bySHA[commit.SHA] = commit
|
|
}
|
|
missing := map[string]bool{}
|
|
ancestors := make(map[string]map[string]struct{}, len(in.Commits))
|
|
var walk func(sha string) map[string]struct{}
|
|
walk = func(sha string) map[string]struct{} {
|
|
if set, ok := ancestors[sha]; ok {
|
|
return set
|
|
}
|
|
set := map[string]struct{}{sha: {}}
|
|
ancestors[sha] = set // 先占位,DAG 不会有环
|
|
commit, ok := bySHA[sha]
|
|
if !ok {
|
|
return set
|
|
}
|
|
for _, parent := range commit.Parents {
|
|
if _, known := bySHA[parent]; !known {
|
|
missing[parent] = true
|
|
continue
|
|
}
|
|
for item := range walk(parent) {
|
|
set[item] = struct{}{}
|
|
}
|
|
}
|
|
return set
|
|
}
|
|
for _, commit := range in.Commits {
|
|
walk(commit.SHA)
|
|
}
|
|
|
|
tagsByTarget := map[string][]RawTag{}
|
|
tagByName := map[string]RawTag{}
|
|
for _, tag := range in.Tags {
|
|
tagsByTarget[tag.TargetSHA] = append(tagsByTarget[tag.TargetSHA], tag)
|
|
if _, exists := tagByName[tag.Name]; !exists {
|
|
tagByName[tag.Name] = tag
|
|
}
|
|
}
|
|
|
|
changelogCache := map[string]string{}
|
|
readChangelog := func(sha string) string {
|
|
if text, ok := changelogCache[sha]; ok {
|
|
return text
|
|
}
|
|
text := ""
|
|
if in.Changelog != nil {
|
|
if loaded, err := in.Changelog(sha); err == nil {
|
|
text = loaded
|
|
}
|
|
}
|
|
changelogCache[sha] = text
|
|
return text
|
|
}
|
|
|
|
graphComplete := len(missing) == 0
|
|
result := DeriveResult{GraphComplete: graphComplete}
|
|
for sha := range missing {
|
|
result.MissingParents = append(result.MissingParents, sha)
|
|
}
|
|
sort.Strings(result.MissingParents)
|
|
|
|
records := make([]governance.VersionHistoryCommit, 0, len(in.Commits))
|
|
for _, commit := range in.Commits {
|
|
changelog := readChangelog(commit.SHA)
|
|
top := governance.ChangelogTopVersion(changelog)
|
|
|
|
baseTagName := ""
|
|
var sinceBase *int
|
|
tagCreatedLater := false
|
|
if governance.SemverValid(top) {
|
|
candidate := "v" + top
|
|
if base, exists := tagByName[candidate]; exists {
|
|
own, ownKnown := ancestors[commit.SHA]
|
|
_, tagIsAncestor := own[base.TargetSHA]
|
|
switch {
|
|
case !graphComplete:
|
|
// tag 存在但图不完整 ⇒ 不给号(宁可没号,也不报错号)
|
|
baseTagName = candidate
|
|
case !ownKnown:
|
|
// 连自己的祖先都给不出 ⇒ 不给号
|
|
case !tagIsAncestor:
|
|
// **该 tag 是更晚才打在这个提交之后的**:这个提交当时还没有发布基线。
|
|
// (提交数在这里算会得到 0 —— 因为它的祖先全被那个更晚的 tag 覆盖 —— 拿它编号是错的。)
|
|
tagCreatedLater = true
|
|
default:
|
|
baseSet := ancestors[base.TargetSHA]
|
|
count := 0
|
|
for item := range own {
|
|
if _, inBase := baseSet[item]; !inBase {
|
|
count++
|
|
}
|
|
}
|
|
baseTagName = candidate
|
|
sinceBase = &count
|
|
}
|
|
}
|
|
}
|
|
|
|
evidenceTags := make([]governance.VersionTagEvidence, 0, len(tagsByTarget[commit.SHA]))
|
|
for _, tag := range tagsByTarget[commit.SHA] {
|
|
url := ""
|
|
if in.TagURLBase != "" {
|
|
url = in.TagURLBase + tag.Name
|
|
}
|
|
evidenceTags = append(evidenceTags, governance.VersionTagEvidence{
|
|
Name: tag.Name, TagSHA: tag.TagSHA, Annotated: tag.Annotated, URL: url})
|
|
}
|
|
sort.Slice(evidenceTags, func(i, j int) bool { return evidenceTags[i].Name < evidenceTags[j].Name })
|
|
|
|
label, state, kind, evidence := governance.DecideVersion(top, evidenceTags, baseTagName, sinceBase)
|
|
switch {
|
|
case label != "":
|
|
case !graphComplete:
|
|
evidence.Reason = governance.ReasonIncompleteGraph
|
|
case tagCreatedLater:
|
|
evidence.Reason = governance.ReasonTagCreatedLater
|
|
}
|
|
record := governance.VersionHistoryCommit{
|
|
RepoKey: in.RepoKey,
|
|
CommitSHA: commit.SHA,
|
|
Parents: commit.Parents,
|
|
CommittedAt: commit.CommittedAt,
|
|
AuthorDisplay: commit.AuthorDisplay,
|
|
Subject: commit.Subject,
|
|
Body: commit.Body,
|
|
VersionLabel: label,
|
|
VersionState: state,
|
|
RecordKind: kind,
|
|
ReleaseTags: evidenceTags,
|
|
CommitURL: in.CommitURLBase + commit.SHA,
|
|
VersionEvidence: evidence,
|
|
SnapshotID: in.SnapshotID,
|
|
}
|
|
if kind == governance.RecordKindRelease {
|
|
record.ReleaseNotes = governance.ExtractReleaseNotes(changelog, label)
|
|
} else if kind == governance.RecordKindDevelopment {
|
|
record.ReleaseNotes = governance.ExtractDevelopmentNotes(changelog, label)
|
|
if record.ReleaseNotes == "" {
|
|
record.ReleaseNotes = governance.ExtractDevelopmentNotesForCommit(in.DeliveryArchive, label, commit.SHA)
|
|
}
|
|
if record.ReleaseNotes == "" {
|
|
for _, tag := range tagsByTarget[commit.SHA] {
|
|
if tag.Annotated && tag.Name == "snapshot/v"+label {
|
|
_, body, found := strings.Cut(tag.Message, "\n\n")
|
|
if found {
|
|
record.ReleaseNotes = strings.TrimSpace(body)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
|
|
// 最新在上;同一提交时间用 SHA 兜底 ⇒ 排序确定,分页不重不漏。
|
|
sort.SliceStable(records, func(i, j int) bool {
|
|
if !records[i].CommittedAt.Equal(records[j].CommittedAt) {
|
|
return records[i].CommittedAt.After(records[j].CommittedAt)
|
|
}
|
|
return records[i].CommitSHA > records[j].CommitSHA
|
|
})
|
|
result.Records = records
|
|
return result
|
|
}
|