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}