add trailer metadata to commit page, fix up styles
feat: add Trailer struct and Trailers field to CommitData feat: add parseTrailers function to extract git trailers from messages feat: populate Trailers field in CommitData instances feat: display commit trailers in commit detail page metadata test commit with trailers This is a test commit to verify trailers display. Signed-off-by: Test User <[email protected]> Co-authored-by: Another Developer <[email protected]> Fixes: #123 test commit with trailers This is a test commit to verify trailers display. Signed-off-by: Test User <[email protected]> Co-authored-by: Another Developer <[email protected]> Fixes: #123 fix: handle trailing newlines in parseTrailers function feat: strip trailers from commit message body, display only in metadata fix: show only summary (first line) in log page commit list feat: split commit message into summary and body with separate styling refactor: remove unused MessageBody field from CommitData
7 files changed,  +119, -14
M html/commit.page.tmpl
+10, -1
 1@@ -4,7 +4,11 @@
 2 
 3 {{define "content"}}
 4   <div>
 5-    <div class="commit-message">{{.Commit.Message}}</div>
 6+    <div class="commit-message"><div class="commit-message__summary">{{.Commit.SummaryStr}}</div>
 7+      {{- if .Commit.MessageBodyOnly -}}
 8+      <div class="commit-message__body">{{.Commit.MessageBodyOnly}}</div>
 9+      {{- end -}}
10+    </div>
11 
12     <div class="metadata">
13       <div class="metadata__label">commit</div>
14@@ -18,6 +22,11 @@
15 
16       <div class="metadata__label">date</div>
17       <div class="metadata__value">{{.Commit.WhenISO}}</div>
18+
19+      {{- range .Commit.Trailers -}}
20+      <div class="metadata__label">{{.Key}}</div>
21+      <div class="metadata__value">{{.Value}}</div>
22+      {{- end -}}
23     </div>
24   </div>
25 
M html/log.page.tmpl
+1, -1
1@@ -10,7 +10,7 @@
2       <div class="commit-item">
3         <div class="commit-item__header">
4           <div class="commit-item__content">
5-            <div class="commit-item__message"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-commit-vertical-icon lucide-git-commit-vertical"><path d="M12 3v6"/><circle cx="12" cy="12" r="3"/><path d="M12 15v6"/></svg><a href="{{.URL}}">{{.Message}}</a></div>
6+            <div class="commit-item__message"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-commit-vertical-icon lucide-git-commit-vertical"><path d="M12 3v6"/><circle cx="12" cy="12" r="3"/><path d="M12 15v6"/></svg><a href="{{.URL}}">{{.SummaryStr}}</a></div>
7             <div class="commit-item__meta">
8               <span class="font-bold">{{.AuthorStr}}</span>
9               <span>&nbsp;&centerdot;&nbsp;</span>
M main.go
+98, -10
  1@@ -13,6 +13,7 @@ import (
  2 	"math"
  3 	"os"
  4 	"path/filepath"
  5+	"regexp"
  6 	"sort"
  7 	"strings"
  8 	"sync"
  9@@ -120,6 +121,11 @@ type TagData struct {
 10 	URL  template.URL
 11 }
 12 
 13+type Trailer struct {
 14+	Key   string
 15+	Value string
 16+}
 17+
 18 type CommitData struct {
 19 	SummaryStr  string
 20 	URL         template.URL
 21@@ -130,6 +136,9 @@ type CommitData struct {
 22 	ShortID     string
 23 	ParentID    string
 24 	Refs        []*RefInfo
 25+	Trailers    []Trailer
 26+	// MessageBodyOnly is the commit message body without summary line and trailers
 27+	MessageBodyOnly string
 28 	*git.Commit
 29 }
 30 
 31@@ -770,6 +779,82 @@ func (c *Config) getCommitURL(commitID string) template.URL {
 32 	return template.URL(url)
 33 }
 34 
 35+// parseCommitMessage extracts git trailer lines from a commit message and returns
 36+// the trailers and the message body without summary line and trailers.
 37+// Trailers are lines at the end of the message in "Key: value" format.
 38+func parseCommitMessage(message string) ([]Trailer, string) {
 39+	var trailers []Trailer
 40+
 41+	// Trailer pattern: key with alphanumeric/hyphens, colon, space, value
 42+	// Examples: "Signed-off-by: John Doe", "Co-authored-by: Jane Smith"
 43+	trailerRe := regexp.MustCompile(`^([A-Za-z0-9-]+): (.+)$`)
 44+
 45+	// Trim trailing newlines to avoid empty last line
 46+	message = strings.TrimRight(message, "\n")
 47+	lines := strings.Split(message, "\n")
 48+
 49+	// Find where trailers start (index of first trailer line)
 50+	trailerStartIdx := -1
 51+
 52+	// Collect trailer lines from the end of the message
 53+	for i := len(lines) - 1; i >= 0; i-- {
 54+		line := strings.TrimSpace(lines[i])
 55+
 56+		// Stop at empty line (separator between message body and trailers)
 57+		if line == "" {
 58+			trailerStartIdx = i + 1
 59+			break
 60+		}
 61+
 62+		matches := trailerRe.FindStringSubmatch(line)
 63+		if matches != nil {
 64+			trailers = append([]Trailer{
 65+				{Key: matches[1], Value: matches[2]},
 66+			}, trailers...)
 67+		} else {
 68+			// Not a trailer line, stop collecting
 69+			trailerStartIdx = i + 1
 70+			break
 71+		}
 72+	}
 73+
 74+	// If no trailers found, treat entire message as body
 75+	bodyLines := lines
 76+	if len(trailers) > 0 && trailerStartIdx > 0 {
 77+		// trailerStartIdx is the first line of trailers, so body is everything before it
 78+		bodyLines = lines[:trailerStartIdx]
 79+	}
 80+
 81+	// Remove trailing empty lines from body
 82+	bodyEndIdx := len(bodyLines)
 83+	for i := len(bodyLines) - 1; i >= 0; i-- {
 84+		if strings.TrimSpace(bodyLines[i]) != "" {
 85+			bodyEndIdx = i + 1
 86+			break
 87+		}
 88+	}
 89+	bodyLines = bodyLines[:bodyEndIdx]
 90+
 91+	// Extract body without summary (everything after first line)
 92+	messageBodyOnly := ""
 93+	if len(bodyLines) > 1 {
 94+		bodyOnlyLines := bodyLines[1:]
 95+		// Remove leading empty lines
 96+		startIdx := 0
 97+		for i, line := range bodyOnlyLines {
 98+			if strings.TrimSpace(line) != "" {
 99+				startIdx = i
100+				break
101+			}
102+		}
103+		if startIdx < len(bodyOnlyLines) {
104+			messageBodyOnly = strings.Join(bodyOnlyLines[startIdx:], "\n")
105+		}
106+	}
107+
108+	return trailers, messageBodyOnly
109+}
110+
111 func (c *Config) getURLs() *SiteURLs {
112 	return &SiteURLs{
113 		HomeURL:    c.HomeURL,
114@@ -1170,17 +1255,20 @@ func (c *Config) writeRevision(repo *git.Repository, pageData *PageData, refs []
115 			} else {
116 				parentID = parentSha.String()
117 			}
118+			trailers, messageBodyOnly := parseCommitMessage(commit.Message)
119 			cd := &CommitData{
120-				ParentID:    parentID,
121-				URL:         c.getCommitURL(commit.ID.String()),
122-				ShortID:     getShortID(commit.ID.String()),
123-				SummaryStr:  commit.Summary(),
124-				AuthorStr:   commit.Author.Name,
125-				WhenStr:     commit.Author.When.Format(time.DateOnly),
126-				WhenISO:     commit.Author.When.UTC().Format(time.RFC3339),
127-				WhenDisplay: formatDateForDisplay(commit.Author.When),
128-				Commit:      commit,
129-				Refs:        tags,
130+				ParentID:        parentID,
131+				URL:             c.getCommitURL(commit.ID.String()),
132+				ShortID:         getShortID(commit.ID.String()),
133+				SummaryStr:      commit.Summary(),
134+				AuthorStr:       commit.Author.Name,
135+				WhenStr:         commit.Author.When.Format(time.DateOnly),
136+				WhenISO:         commit.Author.When.UTC().Format(time.RFC3339),
137+				WhenDisplay:     formatDateForDisplay(commit.Author.When),
138+				Commit:          commit,
139+				Refs:            tags,
140+				Trailers:        trailers,
141+				MessageBodyOnly: messageBodyOnly,
142 			}
143 			logs = append(logs, cd)
144 			if i == 0 {
M static/pgit.css
+9, -1
 1@@ -1104,12 +1104,20 @@ sup {
 2   margin: 0;
 3   margin-bottom: calc(var(--line-height) * 2);
 4   white-space: break-spaces;
 5-  font-weight: bold;
 6   font-size: 1rem;
 7   line-height: var(--line-height);
 8   text-transform: none;
 9 }
10 
11+.commit-message__summary {
12+  font-weight: 700;
13+  margin-bottom: var(--line-height);
14+}
15+
16+.commit-message__body {
17+  font-weight: 400;
18+}
19+
20 /* File List Summary (in commit page) */
21 .file-list-summary {
22   margin-top: var(--grid-height);
A testdata/objects/15/1ecc08e514601f14b9cd2c26d858f54159e2a8
+0, -0
A testdata/objects/da/c8123e5796000d7a1d8acad91a9275ae510fd3
+0, -0
M testdata/refs/heads/main
+1, -1
1@@ -1 +1 @@
2-6b045bbefb0f27ea3972bf1ea5ec74b17111586d
3+dac8123e5796000d7a1d8acad91a9275ae510fd3