{{.Commit.Author.Name}} created
++ {{ $names := .Commit | attributionNames }} + {{ $nlen := len $names }} + {{ range $i, $n := $names }} + {{ if gt $i 0 }} + {{ if eq $nlen 2 }} and {{ else if eq $i (sub $nlen 1) }}, and {{ else }}, {{ end }} + {{ end }} + {{$n}} + {{ end }} + created +
{{$body := .Commit.Message | commitBody}} {{if $body}} diff --git a/pkg/web/templates/commits.html b/pkg/web/templates/commits.html index c5679b20bbf4b443e710acf8f6e9cee2db190265..d0998b47d12756d4dfcae977bc009e44113b3f39 100644 --- a/pkg/web/templates/commits.html +++ b/pkg/web/templates/commits.html @@ -24,7 +24,15 @@ {{end}}- {{.Author.Name}} created + {{ $names := . | attributionNames }} + {{ $nlen := len $names }} + {{ range $i, $n := $names }} + {{ if gt $i 0 }} + {{ if eq $nlen 2 }} and {{ else if eq $i (sub $nlen 1) }}, and {{ else }}, {{ end }} + {{ end }} + {{$n}} + {{ end }} + created
diff --git a/pkg/web/util.go b/pkg/web/util.go index cc2429d633775e7c223c5c11e328c28d0d4647fa..a36794571fcdcfa7d759c90bd38bc2b36c98cf27 100644 --- a/pkg/web/util.go +++ b/pkg/web/util.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net/http" + "strings" ) func renderStatus(code int) http.HandlerFunc { @@ -12,3 +13,100 @@ func renderStatus(code int) http.HandlerFunc { io.WriteString(w, fmt.Sprintf("%d %s", code, http.StatusText(code))) //nolint: errcheck } } + +// ParseTrailers extracts Git commit message trailers from the given message. +// Trailers are key-value pairs at the end of the commit message in the format +// "Key: value". This function returns a map where keys are normalized to lowercase +// and values are arrays to support multiple trailers with the same key. +func ParseTrailers(message string) map[string][]string { + trailers := make(map[string][]string) + + lines := strings.Split(message, "\n") + + // Trim trailing blank lines so we don't select an empty "block" past the end + for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" { + lines = lines[:len(lines)-1] + } + if len(lines) == 0 { + return trailers + } + + // Find the start of the last paragraph (after the last blank line) + start := 0 + for i := len(lines) - 1; i >= 0; i-- { + if strings.TrimSpace(lines[i]) == "" { + start = i + 1 + break + } + } + + // Parse trailers from the trailer block, skipping non-trailer lines + for i := start; i < len(lines); i++ { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + if key, value, ok := parseTrailerLine(line); ok { + key = strings.ToLower(key) + trailers[key] = append(trailers[key], value) + } + } + + return trailers +} + +// parseTrailerLine extracts the key and value from a trailer line. +func parseTrailerLine(line string) (key, value string, ok bool) { + // Split on first : or # + sepIdx := -1 + for i, ch := range line { + if ch == ':' || ch == '#' { + sepIdx = i + break + } + } + + if sepIdx == -1 { + return "", "", false + } + + key = strings.TrimSpace(line[:sepIdx]) + value = strings.TrimSpace(line[sepIdx+1:]) + + if key == "" || value == "" { + return "", "", false + } + + return key, value, true +} + +// GetCoAuthors extracts co-author names from commit message trailers. +// It looks for "Co-authored-by" or "Co-Authored-By" trailers and extracts +// the name portion (without email address). +func GetCoAuthors(message string) []string { + trailers := ParseTrailers(message) + coAuthors := []string{} + + // Look for co-authored-by trailer (case-insensitive) + if authors, ok := trailers["co-authored-by"]; ok { + for _, author := range authors { + // Extract name from "Name