mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
229 lines
6.0 KiB
Go
229 lines
6.0 KiB
Go
package main
|
|
|
|
import (
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// commitEntry represents a single non-merge commit.
|
|
type commitEntry struct {
|
|
SHA string
|
|
FullSHA string
|
|
Title string
|
|
PRCount int // 0 if no PR number found
|
|
Timestamp int64
|
|
}
|
|
|
|
var prNumRe = regexp.MustCompile(`\(#(\d+)\)`)
|
|
|
|
// cherryPickPRRe matches cherry-pick bot titles like
|
|
// "chore: foo bar (cherry-pick #42) (#43)".
|
|
var cherryPickPRRe = regexp.MustCompile(`\(cherry-pick #(\d+)\)\s*\(#\d+\)$`)
|
|
|
|
// commitLog returns non-merge commits in the given range, filtering
|
|
// out left-side commits (already in the base) and deduplicating
|
|
// cherry-picks using git's --cherry-mark.
|
|
func commitLog(commitRange string) ([]commitEntry, error) {
|
|
// Use --left-right --cherry-mark to identify equivalent
|
|
// (cherry-picked) commits and left-side-only commits.
|
|
out, err := gitOutput("log", "--no-merges", "--left-right", "--cherry-mark",
|
|
"--pretty=format:%m %ct %h %H %s", commitRange)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if out == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
// Collect cherry-pick equivalent commits (marked with '=') so
|
|
// we can skip duplicates. We keep only the right-side version.
|
|
seen := make(map[string]bool)
|
|
|
|
var entries []commitEntry
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
// Format: %m %ct %h %H %s
|
|
// mark timestamp shortSHA fullSHA title...
|
|
parts := strings.SplitN(line, " ", 5)
|
|
if len(parts) < 5 {
|
|
continue
|
|
}
|
|
mark := parts[0]
|
|
ts, _ := strconv.ParseInt(parts[1], 10, 64)
|
|
shortSHA := parts[2]
|
|
fullSHA := parts[3]
|
|
title := parts[4]
|
|
|
|
// Skip left-side commits (already in the old version).
|
|
if mark == "<" {
|
|
continue
|
|
}
|
|
// Skip cherry-pick equivalents that we've already seen
|
|
// (marked '=' by --cherry-mark).
|
|
if mark == "=" {
|
|
if seen[title] {
|
|
continue
|
|
}
|
|
seen[title] = true
|
|
}
|
|
|
|
// Normalize cherry-pick bot titles:
|
|
// "chore: foo (cherry-pick #42) (#43)" → "chore: foo (#42)"
|
|
if m := cherryPickPRRe.FindStringSubmatch(title); m != nil {
|
|
title = title[:cherryPickPRRe.FindStringIndex(title)[0]] + "(#" + m[1] + ")"
|
|
}
|
|
|
|
e := commitEntry{
|
|
SHA: shortSHA,
|
|
FullSHA: fullSHA,
|
|
Title: title,
|
|
Timestamp: ts,
|
|
}
|
|
if m := prNumRe.FindStringSubmatch(e.Title); m != nil {
|
|
e.PRCount, _ = strconv.Atoi(m[1])
|
|
}
|
|
entries = append(entries, e)
|
|
}
|
|
|
|
// Sort by conventional commit prefix, then by timestamp
|
|
// (matching the bash script's sort -k3,3 -k1,1n).
|
|
sort.SliceStable(entries, func(i, j int) bool {
|
|
pi := commitSortPrefix(entries[i].Title)
|
|
pj := commitSortPrefix(entries[j].Title)
|
|
if pi != pj {
|
|
return pi < pj
|
|
}
|
|
return entries[i].Timestamp < entries[j].Timestamp
|
|
})
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
// commitSortPrefix extracts the first word of a title for sorting.
|
|
func commitSortPrefix(title string) string {
|
|
idx := strings.IndexAny(title, " (:")
|
|
if idx < 0 {
|
|
return title
|
|
}
|
|
return title[:idx]
|
|
}
|
|
|
|
// humanizedAreas maps conventional commit scopes to human-readable area
|
|
// names. Order matters: more specific prefixes must come first so that
|
|
// the first partial match wins.
|
|
var humanizedAreas = []struct {
|
|
Prefix string
|
|
Area string
|
|
}{
|
|
{"agent/agentssh", "Agent SSH"},
|
|
{"coderd/database", "Database"},
|
|
{"enterprise/audit", "Auditing"},
|
|
{"enterprise/cli", "CLI"},
|
|
{"enterprise/coderd", "Server"},
|
|
{"enterprise/dbcrypt", "Database"},
|
|
{"enterprise/derpmesh", "Networking"},
|
|
{"enterprise/provisionerd", "Provisioner"},
|
|
{"enterprise/tailnet", "Networking"},
|
|
{"enterprise/wsproxy", "Workspace Proxy"},
|
|
{"agent", "Agent"},
|
|
{"cli", "CLI"},
|
|
{"coderd", "Server"},
|
|
{"codersdk", "SDK"},
|
|
{"docs", "Documentation"},
|
|
{"enterprise", "Enterprise"},
|
|
{"examples", "Examples"},
|
|
{"helm", "Helm"},
|
|
{"install.sh", "Installer"},
|
|
{"provisionersdk", "SDK"},
|
|
{"provisionerd", "Provisioner"},
|
|
{"provisioner", "Provisioner"},
|
|
{"pty", "CLI"},
|
|
{"scaletest", "Scale Testing"},
|
|
{"site", "Dashboard"},
|
|
{"support", "Support"},
|
|
{"tailnet", "Networking"},
|
|
}
|
|
|
|
// conventionalPrefixRe extracts prefix, scope, and rest from a
|
|
// conventional commit title. Does NOT match breaking "!" suffix —
|
|
// those titles are left as-is (matching bash behavior).
|
|
var conventionalPrefixRe = regexp.MustCompile(`^([a-z]+)(\((.+)\))?:\s*(.*)$`)
|
|
|
|
// humanizeTitle converts a conventional commit title to a
|
|
// human-readable form, e.g. "feat(site): add bar" → "Dashboard: Add bar".
|
|
func humanizeTitle(title string) string {
|
|
m := conventionalPrefixRe.FindStringSubmatch(title)
|
|
if m == nil {
|
|
return title
|
|
}
|
|
scope := m[3] // may be empty
|
|
rest := m[4]
|
|
if rest == "" {
|
|
return title
|
|
}
|
|
// Capitalize the first letter of the rest.
|
|
rest = strings.ToUpper(rest[:1]) + rest[1:]
|
|
|
|
if scope == "" {
|
|
return rest
|
|
}
|
|
|
|
// Look up scope in humanizedAreas (first partial match wins).
|
|
for _, ha := range humanizedAreas {
|
|
if strings.HasPrefix(scope, ha.Prefix) {
|
|
return ha.Area + ": " + rest
|
|
}
|
|
}
|
|
// Scope not found in map — return as-is.
|
|
return title
|
|
}
|
|
|
|
// breakingCommitRe matches conventional commit "!:" breaking changes.
|
|
var breakingCommitRe = regexp.MustCompile(`^[a-zA-Z]+(\(.+\))?!:`)
|
|
|
|
// categorizeCommit determines the release note section for a commit.
|
|
// The priority order matches the bash script: breaking title first,
|
|
// then labels (breaking, security, experimental), then prefix.
|
|
func categorizeCommit(title string, labels []string) string {
|
|
// Check breaking title first (matches bash behavior).
|
|
if breakingCommitRe.MatchString(title) {
|
|
return "breaking"
|
|
}
|
|
|
|
// Label-based categorization.
|
|
for _, l := range labels {
|
|
if l == "release/breaking" {
|
|
return "breaking"
|
|
}
|
|
if l == "security" {
|
|
return "security"
|
|
}
|
|
if l == "release/experimental" {
|
|
return "experimental"
|
|
}
|
|
}
|
|
|
|
// Extract the conventional commit prefix (e.g. "feat", "fix(scope)").
|
|
prefixRe := regexp.MustCompile(`^([a-z]+)(\(.+\))?[!]?:`)
|
|
m := prefixRe.FindStringSubmatch(title)
|
|
if m == nil {
|
|
return "other"
|
|
}
|
|
|
|
validPrefixes := []string{
|
|
"feat", "fix", "docs", "refactor", "perf",
|
|
"test", "build", "ci", "chore", "revert",
|
|
}
|
|
for _, p := range validPrefixes {
|
|
if m[1] == p {
|
|
return p
|
|
}
|
|
}
|
|
return "other"
|
|
}
|