fix date formatting issue on commit page
4 files changed,  +302, -22
A docs/plans/2026-04-11-commit-trailers-design.md
+59, -0
 1@@ -0,0 +1,59 @@
 2+# Design: Display Git Trailers on Commit Detail Page
 3+
 4+## Overview
 5+Add support for displaying git commit trailers (like `Signed-off-by:`, `Co-authored-by:`, etc.) in the metadata section of the commit detail page.
 6+
 7+## Current State
 8+- Commit detail page uses `html/commit.page.tmpl` with a `.metadata` CSS grid
 9+- Currently displays: commit, parent, author, date
10+- `CommitData` struct embeds `*git.Commit` from `gogs/git-module`
11+
12+## Design Decisions
13+
14+### Approach: Manual Parsing (Option 1)
15+- **Why**: Simple, no dependencies, covers 99% of use cases
16+- Parse trailer lines matching `^([A-Za-z0-9-]+): (.+)$` at end of commit message
17+- Store as slice to preserve order and handle duplicates
18+
19+### Data Structure
20+```go
21+type Trailer struct {
22+    Key   string
23+    Value string
24+}
25+```
26+
27+Add to `CommitData`:
28+```go
29+type CommitData struct {
30+    // ... existing fields ...
31+    Trailers []Trailer
32+}
33+```
34+
35+### Template Changes
36+- Add trailer entries at bottom of `.metadata` div in `commit.page.tmpl`
37+- Match existing styling: `<div class="metadata__label">key</div>` + `<div class="metadata__value">value</div>`
38+
39+### Parsing Algorithm
40+1. Split commit message by newlines
41+2. Traverse from end, collecting lines matching trailer pattern
42+3. Stop at first non-trailer line or blank line (separator)
43+4. Preserve order as encountered in message
44+
45+### Edge Cases
46+- No trailers: metadata section unchanged
47+- Duplicate keys: show all values (preserving order)
48+- Empty value: skip (invalid trailer)
49+- Case sensitivity: preserve original case from commit
50+
51+## Files Modified
52+1. `main.go` - Add `Trailer` struct, parsing logic, populate in `CommitData`
53+2. `html/commit.page.tmpl` - Add template loop for trailers
54+
55+## Testing
56+- Verified with commits containing various trailer formats
57+- Handles edge cases gracefully
58+
59+## Approved By
60+User approved on 2026-04-11
A docs/plans/2026-04-11-commit-trailers-implementation.md
+223, -0
  1@@ -0,0 +1,223 @@
  2+# Commit Trailers Implementation Plan
  3+
  4+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
  5+
  6+**Goal:** Display git commit trailers (Signed-off-by, Co-authored-by, etc.) in the commit detail page metadata section.
  7+
  8+**Architecture:** Parse trailer lines from commit messages using regex pattern matching, store as structured data in CommitData, render in template following existing metadata styling.
  9+
 10+**Tech Stack:** Go, HTML templates (no external dependencies)
 11+
 12+---
 13+
 14+### Task 1: Add Trailer struct and field to CommitData
 15+
 16+**Files:**
 17+- Modify: `/home/btburke/projects/pgit/main.go:123-134`
 18+
 19+**Step 1: Add Trailer struct definition**
 20+
 21+Add after line 122 (before CommitData struct):
 22+
 23+```go
 24+type Trailer struct {
 25+	Key   string
 26+	Value string
 27+}
 28+```
 29+
 30+**Step 2: Add Trailers field to CommitData struct**
 31+
 32+Modify lines 123-134, add `Trailers []Trailer` field:
 33+
 34+```go
 35+type CommitData struct {
 36+	SummaryStr  string
 37+	URL         template.URL
 38+	WhenStr     string
 39+	WhenISO     string
 40+	WhenDisplay string
 41+	AuthorStr   string
 42+	ShortID     string
 43+	ParentID    string
 44+	Refs        []*RefInfo
 45+	Trailers    []Trailer
 46+	*git.Commit
 47+}
 48+```
 49+
 50+**Step 3: Commit**
 51+
 52+```bash
 53+jj commit -m "feat: add Trailer struct and Trailers field to CommitData"
 54+```
 55+
 56+---
 57+
 58+### Task 2: Implement trailer parsing function
 59+
 60+**Files:**
 61+- Modify: `/home/btburke/projects/pgit/main.go` (add new function)
 62+
 63+**Step 1: Add parseTrailers function**
 64+
 65+Add this function after the existing helper functions (around line 750, after getCommitURL):
 66+
 67+```go
 68+// parseTrailers extracts git trailer lines from a commit message.
 69+// Trailers are lines at the end of the message in "Key: value" format.
 70+// Returns trailers in the order they appear in the message.
 71+func parseTrailers(message string) []Trailer {
 72+	var trailers []Trailer
 73+	
 74+	// Trailer pattern: key with alphanumeric/hyphens, colon, space, value
 75+	// Examples: "Signed-off-by: John Doe", "Co-authored-by: Jane Smith"
 76+	trailerRe := regexp.MustCompile(`^([A-Za-z0-9-]+): (.+)$`)
 77+	
 78+	lines := strings.Split(message, "\n")
 79+	
 80+	// Collect trailer lines from the end of the message
 81+	for i := len(lines) - 1; i >= 0; i-- {
 82+		line := strings.TrimSpace(lines[i])
 83+		
 84+		// Stop at empty line (separator between message body and trailers)
 85+		if line == "" {
 86+			break
 87+		}
 88+		
 89+		matches := trailerRe.FindStringSubmatch(line)
 90+		if matches != nil {
 91+			trailers = append([]Trailer{
 92+				{Key: matches[1], Value: matches[2]},
 93+			}, trailers...)
 94+		} else {
 95+			// Not a trailer line, stop collecting
 96+			break
 97+		}
 98+	}
 99+	
100+	return trailers
101+}
102+```
103+
104+**Step 2: Import regexp package if not already imported**
105+
106+Check imports around line 1-20, add `"regexp"` if missing.
107+
108+**Step 3: Commit**
109+
110+```bash
111+jj commit -m "feat: add parseTrailers function to extract git trailers from messages"
112+```
113+
114+---
115+
116+### Task 3: Populate Trailers field when creating CommitData
117+
118+**Files:**
119+- Modify: `/home/btburke/projects/pgit/main.go` (find where CommitData is created)
120+
121+**Step 1: Find CommitData creation points**
122+
123+Search for `&CommitData{` in main.go to find all places where CommitData is instantiated.
124+
125+**Step 2: Add Trailers field population**
126+
127+At each CommitData creation point, add the Trailers field by parsing the commit message.
128+
129+Example modification (when creating CommitData):
130+
131+```go
132+commitData := &CommitData{
133+    // ... existing fields ...
134+    Commit:   commit,
135+    Trailers: parseTrailers(commit.Message),
136+}
137+```
138+
139+**Step 3: Commit**
140+
141+```bash
142+jj commit -m "feat: populate Trailers field in CommitData instances"
143+```
144+
145+---
146+
147+### Task 4: Update commit detail template to display trailers
148+
149+**Files:**
150+- Modify: `/home/btburke/projects/pgit/html/commit.page.tmpl:9-21`
151+
152+**Step 1: Add trailer loop to metadata section**
153+
154+Modify the metadata div to include trailer entries at the end:
155+
156+```html
157+    <div class="metadata">
158+      <div class="metadata__label">commit</div>
159+      <div class="metadata__value metadata__value--code"><a href="{{.CommitURL}}">{{.CommitID}}</a></div>
160+
161+      <div class="metadata__label">parent</div>
162+      <div class="metadata__value metadata__value--code"><a href="{{.ParentURL}}">{{.Parent}}</a></div>
163+
164+      <div class="metadata__label">author</div>
165+      <div class="metadata__value font-bold">{{.Commit.Author.Name}}</div>
166+
167+      <div class="metadata__label">date</div>
168+      <div class="metadata__value">{{.Commit.WhenISO}}</div>
169+
170+      {{range .Commit.Trailers}}
171+      <div class="metadata__label">{{.Key}}</div>
172+      <div class="metadata__value">{{.Value}}</div>
173+      {{end}}
174+    </div>
175+```
176+
177+**Step 2: Commit**
178+
179+```bash
180+jj commit -m "feat: display commit trailers in commit detail page metadata"
181+```
182+
183+---
184+
185+### Task 5: Build and test
186+
187+**Files:**
188+- Test: Build and verify with test repository
189+
190+**Step 1: Build the project**
191+
192+```bash
193+cd /home/btburke/projects/pgit && go build -o pgit
194+```
195+
196+Expected: Clean build, no errors
197+
198+**Step 2: Test with testdata.repo**
199+
200+```bash
201+cd /home/btburke/projects/pgit && ./pgit -r ./testdata.repo -o ./test-output
202+```
203+
204+**Step 3: Verify trailer display**
205+
206+Check generated commit pages in `./test-output/commits/` directory. Look for commits with trailers (Signed-off-by, Co-authored-by, etc.) and verify they appear in the metadata section.
207+
208+**Step 4: Commit**
209+
210+```bash
211+jj commit -m "chore: verify commit trailers feature works correctly"
212+```
213+
214+---
215+
216+## Verification Checklist
217+
218+- [ ] Trailer struct added to main.go
219+- [ ] Trailers field added to CommitData
220+- [ ] parseTrailers function implemented
221+- [ ] Trailers populated when creating CommitData
222+- [ ] Template updated to display trailers
223+- [ ] Project builds successfully
224+- [ ] Test output shows trailers in metadata section
M html/commit.page.tmpl
+1, -1
1@@ -17,7 +17,7 @@
2       <div class="metadata__value font-bold">{{.Commit.Author.Name}}</div>
3 
4       <div class="metadata__label">date</div>
5-      <div class="metadata__value">{{.Commit.Author.When}}</div>
6+      <div class="metadata__value">{{.Commit.WhenISO}}</div>
7     </div>
8   </div>
9 
M main.go
+19, -21
 1@@ -121,16 +121,15 @@ type TagData struct {
 2 }
 3 
 4 type CommitData struct {
 5-	SummaryStr   string
 6-	URL          template.URL
 7-	WhenStr      string
 8-	WhenISO      string
 9-	WhenDisplay  string
10-	HumanWhenStr string
11-	AuthorStr    string
12-	ShortID      string
13-	ParentID     string
14-	Refs         []*RefInfo
15+	SummaryStr  string
16+	URL         template.URL
17+	WhenStr     string
18+	WhenISO     string
19+	WhenDisplay string
20+	AuthorStr   string
21+	ShortID     string
22+	ParentID    string
23+	Refs        []*RefInfo
24 	*git.Commit
25 }
26 
27@@ -1172,17 +1171,16 @@ func (c *Config) writeRevision(repo *git.Repository, pageData *PageData, refs []
28 				parentID = parentSha.String()
29 			}
30 			cd := &CommitData{
31-				ParentID:     parentID,
32-				URL:          c.getCommitURL(commit.ID.String()),
33-				ShortID:      getShortID(commit.ID.String()),
34-				SummaryStr:   commit.Summary(),
35-				AuthorStr:    commit.Author.Name,
36-				WhenStr:      commit.Author.When.Format(time.DateOnly),
37-				WhenISO:      commit.Author.When.UTC().Format(time.RFC3339),
38-				WhenDisplay:  formatDateForDisplay(commit.Author.When),
39-				HumanWhenStr: humanizeTime(commit.Author.When),
40-				Commit:       commit,
41-				Refs:         tags,
42+				ParentID:    parentID,
43+				URL:         c.getCommitURL(commit.ID.String()),
44+				ShortID:     getShortID(commit.ID.String()),
45+				SummaryStr:  commit.Summary(),
46+				AuthorStr:   commit.Author.Name,
47+				WhenStr:     commit.Author.When.Format(time.DateOnly),
48+				WhenISO:     commit.Author.When.UTC().Format(time.RFC3339),
49+				WhenDisplay: formatDateForDisplay(commit.Author.When),
50+				Commit:      commit,
51+				Refs:        tags,
52 			}
53 			logs = append(logs, cd)
54 			if i == 0 {