识别“## 开发快照 <version>(说明)”并保留旧 snapshot/v 格式。 版本号精确匹配、不跨二级标题、空正文不返回,archive 精确 SHA 绑定不变。
This commit is contained in:
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 开发快照 0.1.352(版本说明标题格式兼容)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **版本页可展开开发说明**:识别 `## 开发快照 <version>(说明)` 标题,与旧
|
||||
`## 开发快照 snapshot/v<version> - ...` 并存;版本号精确匹配,`0.1.35` 不会命中
|
||||
`0.1.351`,只读取到下一个二级标题,空正文不返回。
|
||||
- 历史提交仍按 commit-specific CHANGELOG、exact-SHA archive、annotated snapshot tag
|
||||
顺序取说明;archive 的 `目标提交` 绑定与正式版基线不变,未补写历史说明。
|
||||
|
||||
## 开发快照 0.1.351(索引预检使用目标映射 ID)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -16,9 +16,8 @@ func developmentNotes(changelog, version, sha string) string {
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(changelog, "\n")
|
||||
prefix := "## 开发快照 snapshot/v" + version + " - "
|
||||
for i, line := range lines {
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
if developmentHeadingMatches(line, version) {
|
||||
end := len(lines)
|
||||
for j := i + 1; j < len(lines); j++ {
|
||||
if strings.HasPrefix(lines[j], "## ") {
|
||||
@@ -39,3 +38,33 @@ func developmentNotes(changelog, version, sha string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// developmentHeadingMatches 接受两种真实标题:
|
||||
// - 旧格式:## 开发快照 snapshot/v0.1.1 - 2026-10-18
|
||||
// - 开发快照:## 开发快照 0.1.351(索引预检使用目标映射 ID)
|
||||
//
|
||||
// 版本号后必须是字符串结束或分隔符,避免 0.1.35 命中 0.1.351;
|
||||
// 只读取到下一个二级标题,不跨版本吞正文。
|
||||
func developmentHeadingMatches(line, version string) bool {
|
||||
prefixes := []string{
|
||||
"## 开发快照 snapshot/v" + version,
|
||||
"## 开发快照 " + version,
|
||||
}
|
||||
for _, prefix := range prefixes {
|
||||
if !strings.HasPrefix(line, prefix) {
|
||||
continue
|
||||
}
|
||||
rest := line[len(prefix):]
|
||||
if rest == "" {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(rest, "—") || strings.HasPrefix(rest, "(") {
|
||||
return true
|
||||
}
|
||||
switch rest[0] {
|
||||
case ' ', '\t', '-', '(':
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package versionhistory
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"knowledge-governance/internal/governance"
|
||||
)
|
||||
|
||||
const plainNotesFixture = `# Changelog
|
||||
|
||||
## 开发快照 0.1.351(索引预检使用目标映射 ID)
|
||||
|
||||
### Fixed
|
||||
|
||||
- 索引预检使用目标映射 ID
|
||||
|
||||
## 开发快照 0.1.35(前缀相似版本)
|
||||
|
||||
### Fixed
|
||||
|
||||
- 不应被 0.1.351 命中
|
||||
|
||||
## 开发快照 0.1.350(首次复用取消本库历史来源门槛)
|
||||
|
||||
### Changed
|
||||
|
||||
- 首次复用说明
|
||||
`
|
||||
|
||||
func TestPlainDevelopmentHeadingExactVersion(t *testing.T) {
|
||||
got := governance.ExtractDevelopmentNotes(plainNotesFixture, "0.1.351")
|
||||
if got == "" || !strings.Contains(got, "索引预检使用目标映射 ID") || strings.Contains(got, "首次复用说明") {
|
||||
t.Fatalf("plain 0.1.351 notes wrong: %q", got)
|
||||
}
|
||||
if other := governance.ExtractDevelopmentNotes(plainNotesFixture, "0.1.35"); strings.Contains(other, "索引预检") {
|
||||
t.Fatalf("0.1.35 must not match 0.1.351 body: %q", other)
|
||||
}
|
||||
if other := governance.ExtractDevelopmentNotes(plainNotesFixture, "0.1.3"); other != "" {
|
||||
t.Fatalf("0.1.3 must not prefix-match 0.1.35/0.1.351: %q", other)
|
||||
}
|
||||
if next := governance.ExtractDevelopmentNotes(plainNotesFixture, "0.1.350"); !strings.Contains(next, "首次复用说明") || strings.Contains(next, "索引预检") {
|
||||
t.Fatalf("adjacent version bleed: %q", next)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlainDevelopmentHeadingEmptyBodyAndLegacyFormat(t *testing.T) {
|
||||
if got := governance.ExtractDevelopmentNotes("## 开发快照 0.1.351(空)\n\n## 开发快照 0.1.350(下一版)\n\n- x\n", "0.1.351"); got != "" {
|
||||
t.Fatalf("empty body must stay empty: %q", got)
|
||||
}
|
||||
legacy := "## 开发快照 snapshot/v0.1.1 - 2026-10-18\n\n### Fixed\n\n- legacy body\n"
|
||||
if got := governance.ExtractDevelopmentNotes(legacy, "0.1.1"); !strings.Contains(got, "legacy body") {
|
||||
t.Fatalf("legacy snapshot format regressed: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealChangelogDevelopmentNotes(t *testing.T) {
|
||||
body, err := os.ReadFile("../../CHANGELOG.md")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
changelog := string(body)
|
||||
for _, tc := range []struct{ version, want string }{
|
||||
{"0.1.351", "索引预检"},
|
||||
{"0.1.350", "首次同步可复用"},
|
||||
} {
|
||||
notes := governance.ExtractDevelopmentNotes(changelog, tc.version)
|
||||
if notes == "" || !strings.Contains(notes, tc.want) {
|
||||
t.Fatalf("real CHANGELOG %s notes missing: %q", tc.version, notes)
|
||||
}
|
||||
if governance.ExtractDevelopmentNotes(changelog, "0.1.35") != "" {
|
||||
t.Fatal("prefix version matched a longer version")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveVersionsPlainHeadingReleaseNotes(t *testing.T) {
|
||||
text := "# Changelog\n\n## 开发快照 0.1.1(索引预检使用目标映射 ID)\n\n### Fixed\n\n- plain delivered notes\n\n## 开发快照 snapshot/v0.1.1 - 2026-10-18\n\n### Fixed\n\n- legacy duplicate heading\n\n## [0.1.0] - 2026-10-13\n\n### Added\n\n- baseline\n"
|
||||
result := DeriveVersions(DeriveInput{
|
||||
Commits: []RawCommit{{SHA: "a", CommittedAt: time.Now()}, {SHA: "b", Parents: []string{"a"}, CommittedAt: time.Now()}},
|
||||
Tags: []RawTag{{Name: "v0.1.0", TargetSHA: "a", Annotated: true}},
|
||||
Changelog: func(string) (string, error) {
|
||||
return text, nil
|
||||
},
|
||||
})
|
||||
for _, record := range result.Records {
|
||||
if record.CommitSHA == "b" {
|
||||
if record.RecordKind != governance.RecordKindDevelopment || !strings.Contains(record.ReleaseNotes, "plain delivered notes") ||
|
||||
strings.Contains(record.ReleaseNotes, "legacy duplicate") {
|
||||
t.Fatalf("DeriveVersions plain heading notes wrong: %q kind=%s label=%s", record.ReleaseNotes, record.RecordKind, record.VersionLabel)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("development record b missing")
|
||||
}
|
||||
|
||||
func TestPlainDevelopmentArchiveBindsExactCommit(t *testing.T) {
|
||||
sha := strings.Repeat("a", 40)
|
||||
archive := "## 开发快照 0.1.351(索引预检使用目标映射 ID)\n\n目标提交:" + sha + "\n\n### Fixed\n\n- archived plain notes\n"
|
||||
if got := governance.ExtractDevelopmentNotesForCommit(archive, "0.1.351", sha); !strings.Contains(got, "archived plain notes") {
|
||||
t.Fatalf("plain archive notes missing for exact commit: %q", got)
|
||||
}
|
||||
if got := governance.ExtractDevelopmentNotesForCommit(archive, "0.1.351", strings.Repeat("b", 40)); got != "" {
|
||||
t.Fatalf("plain archive notes leaked to wrong commit: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealChangelogVersionInventory(t *testing.T) {
|
||||
body, err := os.ReadFile("../../CHANGELOG.md")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
changelog := string(body)
|
||||
for _, version := range []string{"0.1.351", "0.1.350", "0.1.349", "0.1.348"} {
|
||||
notes := governance.ExtractDevelopmentNotes(changelog, version)
|
||||
heading := ""
|
||||
for _, line := range strings.Split(changelog, "\n") {
|
||||
if strings.HasPrefix(line, "## 开发快照 "+version) || strings.HasPrefix(line, "## 开发快照 snapshot/v"+version) {
|
||||
heading = line
|
||||
break
|
||||
}
|
||||
}
|
||||
t.Logf("version=%s heading=%q notes_bytes=%d", version, heading, len(notes))
|
||||
if version == "0.1.351" || version == "0.1.350" {
|
||||
if notes == "" {
|
||||
t.Fatalf("%s has a CHANGELOG section but no extracted notes", version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user