1package web
2
3import (
4 "fmt"
5 "io"
6 "net/http"
7 "strings"
8)
9
10func renderStatus(code int) http.HandlerFunc {
11 return func(w http.ResponseWriter, _ *http.Request) {
12 w.WriteHeader(code)
13 io.WriteString(w, fmt.Sprintf("%d %s", code, http.StatusText(code))) //nolint: errcheck
14 }
15}
16
17// ParseTrailers extracts Git commit message trailers from the given message.
18// Trailers are key-value pairs at the end of the commit message in the format
19// "Key: value". This function returns a map where keys are normalized to lowercase
20// and values are arrays to support multiple trailers with the same key.
21func ParseTrailers(message string) map[string][]string {
22 trailers := make(map[string][]string)
23
24 lines := strings.Split(message, "\n")
25
26 // Trim trailing blank lines so we don't select an empty "block" past the end
27 for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
28 lines = lines[:len(lines)-1]
29 }
30 if len(lines) == 0 {
31 return trailers
32 }
33
34 // Find the start of the last paragraph (after the last blank line)
35 start := 0
36 for i := len(lines) - 1; i >= 0; i-- {
37 if strings.TrimSpace(lines[i]) == "" {
38 start = i + 1
39 break
40 }
41 }
42
43 // Parse trailers from the trailer block, skipping non-trailer lines
44 for i := start; i < len(lines); i++ {
45 line := strings.TrimSpace(lines[i])
46 if line == "" {
47 continue
48 }
49 if key, value, ok := parseTrailerLine(line); ok {
50 key = strings.ToLower(key)
51 trailers[key] = append(trailers[key], value)
52 }
53 }
54
55 return trailers
56}
57
58// parseTrailerLine extracts the key and value from a trailer line.
59func parseTrailerLine(line string) (key, value string, ok bool) {
60 // Split on first : or #
61 sepIdx := -1
62 for i, ch := range line {
63 if ch == ':' || ch == '#' {
64 sepIdx = i
65 break
66 }
67 }
68
69 if sepIdx == -1 {
70 return "", "", false
71 }
72
73 key = strings.TrimSpace(line[:sepIdx])
74 value = strings.TrimSpace(line[sepIdx+1:])
75
76 if key == "" || value == "" {
77 return "", "", false
78 }
79
80 return key, value, true
81}
82
83// GetCoAuthors extracts co-author names from commit message trailers.
84// It looks for "Co-authored-by" or "Co-Authored-By" trailers and extracts
85// the name portion (without email address).
86func GetCoAuthors(message string) []string {
87 trailers := ParseTrailers(message)
88 coAuthors := []string{}
89
90 // Look for co-authored-by trailer (case-insensitive)
91 if authors, ok := trailers["co-authored-by"]; ok {
92 for _, author := range authors {
93 // Extract name from "Name <email>" format
94 name := extractName(author)
95 if name != "" {
96 coAuthors = append(coAuthors, name)
97 }
98 }
99 }
100
101 return coAuthors
102}
103
104// extractName extracts the name portion from "Name <email>" format.
105// If no email is present, returns the entire string trimmed.
106func extractName(author string) string {
107 // Look for "Name <email>" pattern
108 if idx := strings.Index(author, "<"); idx != -1 {
109 return strings.TrimSpace(author[:idx])
110 }
111 return strings.TrimSpace(author)
112}