diff --git a/internal/vociferate/vociferate.go b/internal/vociferate/vociferate.go index 4d4fd60..d014006 100644 --- a/internal/vociferate/vociferate.go +++ b/internal/vociferate/vociferate.go @@ -9,6 +9,7 @@ package vociferate import ( "fmt" "os" + "os/exec" "path/filepath" "regexp" "strconv" @@ -16,9 +17,9 @@ import ( ) const ( - defaultVersionFile = "release-version" - defaultVersionExpr = `^\s*([^\r\n]+)\s*$` - defaultChangelog = "changelog.md" + defaultVersionFile = "release-version" + defaultVersionExpr = `^\s*([^\r\n]+)\s*$` + defaultChangelog = "changelog.md" defaultUnreleasedTemplate = "### Breaking\n\n### Added\n\n### Changed\n\n### Removed\n\n### Fixed\n" ) @@ -246,7 +247,7 @@ func updateChangelog(rootDir, version, releaseDate, changelogPath string) error updated := text[:afterHeader] + "\n" + defaultUnreleasedTemplate + "\n" + newSection + text[nextSectionStart:] repoURL, ok := deriveRepositoryURL(rootDir) if ok { - updated = addChangelogLinks(updated, repoURL) + updated = addChangelogLinks(updated, repoURL, rootDir) } if err := os.WriteFile(path, []byte(updated), 0o644); err != nil { return fmt.Errorf("write changelog: %w", err) @@ -426,7 +427,7 @@ func normalizeRepoURL(remoteURL string) (string, bool) { return "", false } -func addChangelogLinks(text, repoURL string) string { +func addChangelogLinks(text, repoURL, rootDir string) string { repoURL = strings.TrimSuffix(strings.TrimSpace(repoURL), "/") if repoURL == "" { return text @@ -456,16 +457,60 @@ func addChangelogLinks(text, repoURL string) string { text = strings.Join(lines[:cutAt], "\n") + "\n" // Build and append reference link definitions. - linkDefs := []string{fmt.Sprintf("[Unreleased]: %s/src/branch/main", repoURL)} - for _, match := range releasedSectionRe.FindAllStringSubmatch(text, -1) { + releasedMatches := releasedSectionRe.FindAllStringSubmatch(text, -1) + releasedVersions := make([]string, 0, len(releasedMatches)) + for _, match := range releasedMatches { if len(match) >= 2 { - linkDefs = append(linkDefs, fmt.Sprintf("[%s]: %s/releases/tag/v%s", match[1], repoURL, match[1])) + releasedVersions = append(releasedVersions, match[1]) } } + linkDefs := make([]string, 0, len(releasedVersions)+1) + if len(releasedVersions) > 0 { + latest := releasedVersions[0] + linkDefs = append(linkDefs, fmt.Sprintf("[Unreleased]: %s/compare/v%s...main", repoURL, latest)) + } else { + linkDefs = append(linkDefs, fmt.Sprintf("[Unreleased]: %s/src/branch/main", repoURL)) + } + + firstCommitShort, hasFirstCommit := firstCommitShortHash(rootDir) + for i, version := range releasedVersions { + if i+1 < len(releasedVersions) { + previousVersion := releasedVersions[i+1] + linkDefs = append(linkDefs, fmt.Sprintf("[%s]: %s/compare/v%s...v%s", version, repoURL, previousVersion, version)) + continue + } + + if hasFirstCommit { + linkDefs = append(linkDefs, fmt.Sprintf("[%s]: %s/compare/%s...v%s", version, repoURL, firstCommitShort, version)) + continue + } + + linkDefs = append(linkDefs, fmt.Sprintf("[%s]: %s/compare/v%s...main", version, repoURL, version)) + } + return strings.TrimRight(text, "\n") + "\n\n" + strings.Join(linkDefs, "\n") + "\n" } +func firstCommitShortHash(rootDir string) (string, bool) { + command := exec.Command("git", "-C", rootDir, "rev-list", "--max-parents=0", "--abbrev-commit", "HEAD") + output, err := command.Output() + if err != nil { + return "", false + } + + commit := strings.TrimSpace(string(output)) + if commit == "" { + return "", false + } + + if strings.Contains(commit, "\n") { + commit = strings.SplitN(commit, "\n", 2)[0] + } + + return commit, true +} + func parseSemver(version string) (semver, error) { parts := strings.Split(strings.TrimSpace(version), ".") if len(parts) != 3 {