From a1ca99d128cbb543f4415633996ebcd863a59b3b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 11:28:41 +0000 Subject: [PATCH 1/2] Diff a -pre.1 changelog entry against the previous tag, not the stable release after it [patch] FindSearchTag looked for "prerelease 0" when resolving the range for vX.Y.Z-pre.1. A stable vX.Y.Z tag parses to the same key, so once the stable release was tagged the -pre.1 entry was diffed against it, got an empty range, and read "No significant changes detected since vX.Y.Z". The first prerelease of a series now diffs against the adjacent earlier tag. Fixes #156 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KV83HNRiTm3h4n1KgMkdqX --- .../Metadata/ChangelogGeneratorTests.cs | 83 +++++++++++++++++++ KtsuBuild/Metadata/ChangelogGenerator.cs | 9 +- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/KtsuBuild.Tests/Metadata/ChangelogGeneratorTests.cs b/KtsuBuild.Tests/Metadata/ChangelogGeneratorTests.cs index d521521..6b2ae16 100644 --- a/KtsuBuild.Tests/Metadata/ChangelogGeneratorTests.cs +++ b/KtsuBuild.Tests/Metadata/ChangelogGeneratorTests.cs @@ -125,6 +125,89 @@ await _generator.GenerateAsync( Assert.IsFalse(content.Contains("Merge pull request"), "Should filter PR merge"); } + private void SetupTaggedHistory(IReadOnlyList tagsNewestFirst, Dictionary> commitsByRange) + { + _gitService.GetTagsAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(tagsNewestFirst)); + foreach (string tag in tagsNewestFirst) + { + _gitService.GetTagCommitHashAsync(Arg.Any(), tag, Arg.Any()) + .Returns(Task.FromResult($"sha-{tag}")); + } + + _gitService.GetCommitsAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(call => Task.FromResult(commitsByRange.TryGetValue(call.ArgAt(1), out IReadOnlyList? commits) ? commits : [])); + } + + private static string GetSection(string changelog, string tag) + { + int start = changelog.IndexOf($"## {tag} ", StringComparison.Ordinal); + Assert.IsTrue(start >= 0, $"Changelog should have an entry for {tag}"); + int end = changelog.IndexOf("## ", start + 3, StringComparison.Ordinal); + return end < 0 ? changelog[start..] : changelog[start..end]; + } + + [TestMethod] + public async Task GenerateAsync_FirstPrereleaseListsCommitsSinceThePreviousTagOnceItsStableTagExists() + { + // Arrange: v1.0.0, then "Add feature A" tagged v1.0.1-pre.1, then "Fix bug B" tagged v1.0.1. + SetupTaggedHistory( + ["v1.0.1", "v1.0.1-pre.1", "v1.0.0"], + new Dictionary> + { + ["sha-v1.0.0..sha-v1.0.1-pre.1"] = [new CommitInfo { Hash = "aaa", Subject = "Add feature A", Author = "developer" }], + ["sha-v1.0.1-pre.1..sha-v1.0.1"] = [new CommitInfo { Hash = "bbb", Subject = "Fix bug B", Author = "developer" }], + ["sha-v1.0.0..sha-v1.0.1"] = + [ + new CommitInfo { Hash = "bbb", Subject = "Fix bug B", Author = "developer" }, + new CommitInfo { Hash = "aaa", Subject = "Add feature A", Author = "developer" }, + ], + }); + + // Act + await _generator.GenerateAsync( + version: "1.0.2", + commitHash: "head", + workingDirectory: "/repo", + outputPath: _tempDir, + lineEnding: "\n").ConfigureAwait(false); + + // Assert + string content = await File.ReadAllTextAsync(Path.Combine(_tempDir, "CHANGELOG.md")).ConfigureAwait(false); + string prerelease = GetSection(content, "v1.0.1-pre.1"); + Assert.IsTrue(prerelease.Contains("Changes since v1.0.0:"), prerelease); + Assert.IsTrue(prerelease.Contains("Add feature A"), prerelease); + Assert.IsFalse(prerelease.Contains("No significant changes"), prerelease); + } + + [TestMethod] + public async Task GenerateAsync_LaterPrereleaseDiffsAgainstThePreviousPrerelease() + { + // Arrange + SetupTaggedHistory( + ["v1.0.1-pre.2", "v1.0.1-pre.1", "v1.0.0"], + new Dictionary> + { + ["sha-v1.0.0..sha-v1.0.1-pre.1"] = [new CommitInfo { Hash = "aaa", Subject = "Add feature A", Author = "developer" }], + ["sha-v1.0.1-pre.1..sha-v1.0.1-pre.2"] = [new CommitInfo { Hash = "bbb", Subject = "Add feature B", Author = "developer" }], + }); + + // Act + await _generator.GenerateAsync( + version: "1.0.1-pre.3", + commitHash: "head", + workingDirectory: "/repo", + outputPath: _tempDir, + lineEnding: "\n").ConfigureAwait(false); + + // Assert + string content = await File.ReadAllTextAsync(Path.Combine(_tempDir, "CHANGELOG.md")).ConfigureAwait(false); + string second = GetSection(content, "v1.0.1-pre.2"); + Assert.IsTrue(second.Contains("Changes since v1.0.1-pre.1:"), second); + Assert.IsTrue(second.Contains("Add feature B"), second); + Assert.IsFalse(second.Contains("Add feature A"), second); + } + [TestMethod] public async Task GenerateAsync_FormatsEntriesCorrectly() { diff --git a/KtsuBuild/Metadata/ChangelogGenerator.cs b/KtsuBuild/Metadata/ChangelogGenerator.cs index 7af5a0d..c813f54 100644 --- a/KtsuBuild/Metadata/ChangelogGenerator.cs +++ b/KtsuBuild/Metadata/ChangelogGenerator.cs @@ -303,7 +303,14 @@ private static string FindSearchTag(IReadOnlyList allTags, string fromTa int searchPatch; int searchPrerelease; - if (toPrerelease != 0) + if (toPrerelease == 1) + { + // The first prerelease of a series has no earlier prerelease to diff against. Searching for + // "prerelease 0" would match the stable X.Y.Z tag, which comes after it, and produce an empty + // range, so diff against the adjacent earlier tag instead. + return fromTag; + } + else if (toPrerelease != 0) { // Prerelease: look for previous prerelease in same series searchMajor = toMajor; From 92a364ac0da06b96b3764ad3fd650bb673213052 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 11:31:15 +0000 Subject: [PATCH 2/2] Fall back to an empty commit list in the changelog test's range lookup Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KV83HNRiTm3h4n1KgMkdqX --- KtsuBuild.Tests/Metadata/ChangelogGeneratorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KtsuBuild.Tests/Metadata/ChangelogGeneratorTests.cs b/KtsuBuild.Tests/Metadata/ChangelogGeneratorTests.cs index 6b2ae16..2e2949d 100644 --- a/KtsuBuild.Tests/Metadata/ChangelogGeneratorTests.cs +++ b/KtsuBuild.Tests/Metadata/ChangelogGeneratorTests.cs @@ -136,7 +136,7 @@ private void SetupTaggedHistory(IReadOnlyList tagsNewestFirst, Dictionar } _gitService.GetCommitsAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(call => Task.FromResult(commitsByRange.TryGetValue(call.ArgAt(1), out IReadOnlyList? commits) ? commits : [])); + .Returns(call => Task.FromResult(commitsByRange.GetValueOrDefault(call.ArgAt(1)) ?? [])); } private static string GetSection(string changelog, string tag)