Skip to content

Commit 1501c1a

Browse files
feat(tui): show per-story commit diff and add pgup/pgdn scrolling
Diff view now shows the commit for the selected user story instead of the entire branch diff. Uses git log --grep to find the story's commit by ID. Also adds pgup/pgdn as alternate key bindings for page scrolling in log and diff views.
1 parent 0977da4 commit 1501c1a

5 files changed

Lines changed: 126 additions & 13 deletions

File tree

internal/git/git.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,52 @@ func GetDiffStats(dir string) (string, error) {
125125
return strings.TrimSpace(string(output)), nil
126126
}
127127

128+
// GetDiffForCommit returns the diff for a single commit.
129+
func GetDiffForCommit(dir, commitHash string) (string, error) {
130+
cmd := exec.Command("git", "diff", commitHash+"~1", commitHash)
131+
cmd.Dir = dir
132+
output, err := cmd.Output()
133+
if err != nil {
134+
// If commitHash~1 doesn't exist (first commit), diff against empty tree
135+
cmd = exec.Command("git", "diff", "4b825dc642cb6eb9a060e54bf899d69f82049674", commitHash)
136+
cmd.Dir = dir
137+
output, err = cmd.Output()
138+
if err != nil {
139+
return "", err
140+
}
141+
}
142+
return string(output), nil
143+
}
144+
145+
// GetDiffStatsForCommit returns the diffstat for a single commit.
146+
func GetDiffStatsForCommit(dir, commitHash string) (string, error) {
147+
cmd := exec.Command("git", "diff", "--stat", commitHash+"~1", commitHash)
148+
cmd.Dir = dir
149+
output, err := cmd.Output()
150+
if err != nil {
151+
cmd = exec.Command("git", "diff", "--stat", "4b825dc642cb6eb9a060e54bf899d69f82049674", commitHash)
152+
cmd.Dir = dir
153+
output, err = cmd.Output()
154+
if err != nil {
155+
return "", err
156+
}
157+
}
158+
return strings.TrimSpace(string(output)), nil
159+
}
160+
161+
// FindCommitForStory searches the git log for a commit matching the given story ID.
162+
// Returns the commit hash if found, empty string otherwise.
163+
func FindCommitForStory(dir, storyID string) (string, error) {
164+
cmd := exec.Command("git", "log", "--grep="+storyID, "--format=%H", "-1")
165+
cmd.Dir = dir
166+
output, err := cmd.Output()
167+
if err != nil {
168+
return "", err
169+
}
170+
hash := strings.TrimSpace(string(output))
171+
return hash, nil
172+
}
173+
128174
// getMergeBase returns the merge base commit between two refs.
129175
func getMergeBase(dir, ref1, ref2 string) (string, error) {
130176
cmd := exec.Command("git", "merge-base", ref1, ref2)

internal/tui/app.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -521,8 +521,19 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
521521
// Diff view
522522
case "d":
523523
if a.viewMode == ViewDashboard || a.viewMode == ViewLog {
524+
// Use the current PRD's worktree directory if available, otherwise base dir
525+
diffDir := a.baseDir
526+
if instance := a.manager.GetInstance(a.prdName); instance != nil && instance.WorktreeDir != "" {
527+
diffDir = instance.WorktreeDir
528+
}
529+
a.diffViewer.SetBaseDir(diffDir)
524530
a.diffViewer.SetSize(a.width-4, a.height-headerHeight-footerHeight-2)
525-
a.diffViewer.Load()
531+
// Load diff for the selected story's commit
532+
if story := a.GetSelectedStory(); story != nil {
533+
a.diffViewer.LoadForStory(story.ID)
534+
} else {
535+
a.diffViewer.Load()
536+
}
526537
a.viewMode = ViewDiff
527538
} else if a.viewMode == ViewDiff {
528539
a.viewMode = ViewDashboard
@@ -606,13 +617,13 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
606617
}
607618

608619
// Log/diff scrolling
609-
case "ctrl+d":
620+
case "ctrl+d", "pgdown":
610621
if a.viewMode == ViewLog {
611622
a.logViewer.PageDown()
612623
} else if a.viewMode == ViewDiff {
613624
a.diffViewer.PageDown()
614625
}
615-
case "ctrl+u":
626+
case "ctrl+u", "pgup":
616627
if a.viewMode == ViewLog {
617628
a.logViewer.PageUp()
618629
} else if a.viewMode == ViewDiff {

internal/tui/dashboard.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -736,11 +736,15 @@ func (a *App) renderDiffHeader() string {
736736
// Branding
737737
brand := headerStyle.Render("chief")
738738

739-
// View indicator
739+
// View indicator - show story ID if viewing a story-specific diff
740+
viewLabel := "[Diff View]"
741+
if a.diffViewer.storyID != "" {
742+
viewLabel = fmt.Sprintf("[Diff: %s]", a.diffViewer.storyID)
743+
}
740744
viewIndicator := lipgloss.NewStyle().
741745
Foreground(PrimaryColor).
742746
Bold(true).
743-
Render("[Diff View]")
747+
Render(viewLabel)
744748

745749
// State indicator
746750
stateStyle := GetStateStyle(a.state)
@@ -787,10 +791,14 @@ func (a *App) renderDiffHeader() string {
787791
func (a *App) renderNarrowDiffHeader() string {
788792
brand := headerStyle.Render("chief")
789793

794+
viewLabel := "[Diff]"
795+
if a.diffViewer.storyID != "" {
796+
viewLabel = fmt.Sprintf("[%s]", a.diffViewer.storyID)
797+
}
790798
viewIndicator := lipgloss.NewStyle().
791799
Foreground(PrimaryColor).
792800
Bold(true).
793-
Render("[Diff]")
801+
Render(viewLabel)
794802

795803
stateStyle := GetStateStyle(a.state)
796804
state := stateStyle.Render(fmt.Sprintf("[%s]", a.state.String()))

internal/tui/diff.go

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type DiffViewer struct {
1515
height int
1616
stats string
1717
baseDir string
18+
storyID string // Story ID whose commit diff is being shown (empty = full branch diff)
1819
err error
1920
loaded bool
2021
}
@@ -32,12 +33,49 @@ func (d *DiffViewer) SetSize(width, height int) {
3233
d.height = height
3334
}
3435

35-
// Load fetches the latest git diff.
36+
// SetBaseDir updates the base directory used for loading diffs.
37+
func (d *DiffViewer) SetBaseDir(dir string) {
38+
d.baseDir = dir
39+
}
40+
41+
// Load fetches the latest git diff for the full branch.
3642
func (d *DiffViewer) Load() {
43+
d.storyID = ""
44+
d.loadDiff("", "")
45+
}
46+
47+
// LoadForStory fetches the git diff for a specific story's commit.
48+
func (d *DiffViewer) LoadForStory(storyID string) {
49+
d.storyID = storyID
50+
51+
// Find the commit for this story
52+
commitHash, err := git.FindCommitForStory(d.baseDir, storyID)
53+
if err != nil || commitHash == "" {
54+
d.offset = 0
55+
d.loaded = true
56+
d.err = nil
57+
d.lines = nil
58+
d.stats = ""
59+
return
60+
}
61+
62+
d.loadDiff(storyID, commitHash)
63+
}
64+
65+
// loadDiff loads a diff, either for a specific commit or the full branch.
66+
func (d *DiffViewer) loadDiff(storyID, commitHash string) {
3767
d.offset = 0
3868
d.loaded = true
3969

40-
diff, err := git.GetDiff(d.baseDir)
70+
var diff string
71+
var err error
72+
73+
if commitHash != "" {
74+
diff, err = git.GetDiffForCommit(d.baseDir, commitHash)
75+
} else {
76+
diff, err = git.GetDiff(d.baseDir)
77+
}
78+
4179
if err != nil {
4280
d.err = err
4381
d.lines = nil
@@ -55,9 +93,16 @@ func (d *DiffViewer) Load() {
5593

5694
d.lines = strings.Split(diff, "\n")
5795

58-
stats, err := git.GetDiffStats(d.baseDir)
59-
if err == nil {
60-
d.stats = stats
96+
if commitHash != "" {
97+
stats, err := git.GetDiffStatsForCommit(d.baseDir, commitHash)
98+
if err == nil {
99+
d.stats = stats
100+
}
101+
} else {
102+
stats, err := git.GetDiffStats(d.baseDir)
103+
if err == nil {
104+
d.stats = stats
105+
}
61106
}
62107
}
63108

@@ -121,6 +166,9 @@ func (d *DiffViewer) Render() string {
121166
}
122167

123168
if len(d.lines) == 0 {
169+
if d.storyID != "" {
170+
return lipgloss.NewStyle().Foreground(MutedColor).Render("No commit found for " + d.storyID)
171+
}
124172
return lipgloss.NewStyle().Foreground(MutedColor).Render("No changes detected")
125173
}
126174

internal/tui/help.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ func (h *HelpOverlay) GetCategories() []ShortcutCategory {
9090
Shortcuts: []Shortcut{
9191
{Key: "j / ↓", Description: "Scroll down"},
9292
{Key: "k / ↑", Description: "Scroll up"},
93-
{Key: "Ctrl+D", Description: "Page down"},
94-
{Key: "Ctrl+U", Description: "Page up"},
93+
{Key: "Ctrl+D / PgDn", Description: "Page down"},
94+
{Key: "Ctrl+U / PgUp", Description: "Page up"},
9595
{Key: "g", Description: "Go to top"},
9696
{Key: "G", Description: "Go to bottom"},
9797
},

0 commit comments

Comments
 (0)