Skip to content

Commit e80adaf

Browse files
committed
Fix blame review follow-ups and persist view modes
1 parent 397c895 commit e80adaf

9 files changed

Lines changed: 159 additions & 12 deletions

File tree

.claude-plugin/skills/revdiff/references/config.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Then uncomment and edit the values you want to change.
2626
| `--wrap` | `REVDIFF_WRAP` | Enable line wrapping in diff view | `false` |
2727
| `--collapsed` | `REVDIFF_COLLAPSED` | Start in collapsed diff mode | `false` |
2828
| `--line-numbers` | `REVDIFF_LINE_NUMBERS` | Show line numbers in diff gutter | `false` |
29+
| `--blame` | `REVDIFF_BLAME` | Show git blame gutter on startup | `false` |
2930
| `--no-confirm-discard` | `REVDIFF_NO_CONFIRM_DISCARD` | Skip confirmation when discarding annotations with Q | `false` |
3031
| `--chroma-style` | `REVDIFF_CHROMA_STYLE` | Chroma color theme for syntax highlighting | `catppuccin-macchiato` |
3132
| `--theme` | `REVDIFF_THEME` | Load color theme from `~/.config/revdiff/themes/` | |

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ Positional arguments support several forms:
165165
| `--wrap` | Enable line wrapping in diff view, env: `REVDIFF_WRAP` | `false` |
166166
| `--collapsed` | Start in collapsed diff mode, env: `REVDIFF_COLLAPSED` | `false` |
167167
| `--line-numbers` | Show line numbers in diff gutter, env: `REVDIFF_LINE_NUMBERS` | `false` |
168+
| `--blame` | Show git blame gutter on startup, env: `REVDIFF_BLAME` | `false` |
168169
| `--no-confirm-discard` | Skip confirmation when discarding annotations with Q, env: `REVDIFF_NO_CONFIRM_DISCARD` | `false` |
169170
| `--chroma-style` | Chroma color theme for syntax highlighting, env: `REVDIFF_CHROMA_STYLE` | `catppuccin-macchiato` |
170171
| `--theme` | Load color theme from `~/.config/revdiff/themes/`, env: `REVDIFF_THEME` | |

cmd/revdiff/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type options struct {
3737
Wrap bool `long:"wrap" ini-name:"wrap" env:"REVDIFF_WRAP" description:"enable line wrapping in diff view"`
3838
Collapsed bool `long:"collapsed" ini-name:"collapsed" env:"REVDIFF_COLLAPSED" description:"start in collapsed diff mode"`
3939
LineNumbers bool `long:"line-numbers" ini-name:"line-numbers" env:"REVDIFF_LINE_NUMBERS" description:"show line numbers in diff gutter"`
40+
Blame bool `long:"blame" ini-name:"blame" env:"REVDIFF_BLAME" description:"show git blame gutter on startup"`
4041
ChromaStyle string `long:"chroma-style" ini-name:"chroma-style" env:"REVDIFF_CHROMA_STYLE" default:"catppuccin-macchiato" description:"chroma style for syntax highlighting"`
4142
AllFiles bool `long:"all-files" short:"A" no-ini:"true" description:"browse all git-tracked files, not just diffs"`
4243
Exclude []string `long:"exclude" short:"X" ini-name:"exclude" env:"REVDIFF_EXCLUDE" env-delim:"," description:"exclude files matching prefix (may be repeated)"`
@@ -296,6 +297,7 @@ func run(opts options) error {
296297
Wrap: opts.Wrap,
297298
Collapsed: opts.Collapsed,
298299
LineNumbers: opts.LineNumbers,
300+
ShowBlame: opts.Blame,
299301
TabWidth: opts.TabWidth,
300302
Ref: opts.ref(),
301303
Staged: opts.Staged,

cmd/revdiff/main_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ func TestParseArgs_Defaults(t *testing.T) {
3535
assert.False(t, opts.NoConfirmDiscard)
3636
assert.False(t, opts.Wrap)
3737
assert.False(t, opts.Collapsed)
38+
assert.False(t, opts.LineNumbers)
39+
assert.False(t, opts.Blame)
3840
assert.Empty(t, opts.Output)
3941
assert.Empty(t, opts.Refs.Base)
4042
assert.Empty(t, opts.Refs.Against)
@@ -140,6 +142,32 @@ func TestParseArgs_LineNumbers(t *testing.T) {
140142
})
141143
}
142144

145+
func TestParseArgs_Blame(t *testing.T) {
146+
t.Run("flag", func(t *testing.T) {
147+
opts, err := parseArgs(append(noConfigArgs(t), "--blame"))
148+
require.NoError(t, err)
149+
assert.True(t, opts.Blame)
150+
})
151+
152+
t.Run("env", func(t *testing.T) {
153+
t.Setenv("REVDIFF_BLAME", "true")
154+
opts, err := parseArgs(noConfigArgs(t))
155+
require.NoError(t, err)
156+
assert.True(t, opts.Blame)
157+
})
158+
159+
t.Run("config file", func(t *testing.T) {
160+
cfgDir := t.TempDir()
161+
cfgPath := filepath.Join(cfgDir, "config")
162+
err := os.WriteFile(cfgPath, []byte("[Application Options]\nblame = true\n"), 0o600)
163+
require.NoError(t, err)
164+
opts, err := parseArgs([]string{"--config", cfgPath})
165+
require.NoError(t, err)
166+
assert.True(t, opts.Blame)
167+
})
168+
}
169+
170+
143171
func TestParseArgs_OutputFlag(t *testing.T) {
144172
opts, err := parseArgs([]string{"-o", "/tmp/out.txt"})
145173
require.NoError(t, err)

diff/blame.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func (g *Git) FileBlame(ref, file string, staged bool) (map[int]BlameLine, error
2626
if err != nil {
2727
return nil, err
2828
}
29-
defer os.Remove(tmpName) //nolint:errcheck // best-effort temp file cleanup
29+
defer func() { _ = os.Remove(tmpName) }()
3030
args = append(args, "--contents", tmpName)
3131
} else if targetRef := blameTargetRef(ref); targetRef != "" {
3232
args = append(args, targetRef)
@@ -51,7 +51,7 @@ func (g *Git) writeStagedBlameFile(file string) (string, error) {
5151
return "", fmt.Errorf("create temp blame file for %s: %w", file, err)
5252
}
5353
if _, err := tmp.WriteString(indexContent); err != nil {
54-
tmp.Close() //nolint:gosec // best-effort close in error path
54+
_ = tmp.Close()
5555
return "", fmt.Errorf("write temp blame file for %s: %w", file, err)
5656
}
5757
if err := tmp.Close(); err != nil {

ui/collapsed.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,7 @@ func (m Model) deletePlaceholderVisualHeight(hunkStart int) int {
180180
return 1
181181
}
182182
text := m.deletePlaceholderText(hunkStart)
183-
gutterExtra := 0
184-
if m.lineNumbers {
185-
gutterExtra = m.lineNumGutterWidth()
186-
}
187-
if m.hasBlameGutter() {
188-
gutterExtra += m.blameGutterWidth()
189-
}
190-
wrapWidth := m.diffContentWidth() - wrapGutterWidth - gutterExtra
183+
wrapWidth := m.diffContentWidth() - wrapGutterWidth - m.gutterExtra()
191184
return len(m.wrapContent(text, wrapWidth))
192185
}
193186

ui/mocks/blamer.go

Lines changed: 84 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/model.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ui
22

33
//go:generate moq -out mocks/renderer.go -pkg mocks -skip-ensure -fmt goimports . Renderer
44
//go:generate moq -out mocks/syntax_highlighter.go -pkg mocks -skip-ensure -fmt goimports . SyntaxHighlighter
5+
//go:generate moq -out mocks/blamer.go -pkg mocks -skip-ensure -fmt goimports . Blamer
56

67
import (
78
"fmt"
@@ -97,7 +98,7 @@ type Model struct {
9798
showBlame bool // true when blame gutter is shown
9899
blameData map[int]diff.BlameLine // blame info keyed by 1-based new line number
99100
blameAuthorLen int // max author display width for blame gutter
100-
blameNow time.Time // snapshot of time.Now() set once per render pass for blame age
101+
blameNow time.Time // snapshot of time.Now() set once per render pass for blame age
101102

102103
searching bool // true when search textinput is active (typing)
103104
searchTerm string // last submitted search query
@@ -154,6 +155,7 @@ type ModelConfig struct {
154155
Wrap bool // enable line wrapping
155156
Collapsed bool // start in collapsed diff mode
156157
LineNumbers bool // show line numbers in diff gutter
158+
ShowBlame bool // show blame gutter on startup when available
157159
Only []string // show only these files (match by exact path or path suffix)
158160
WorkDir string // working directory for resolving absolute --only paths
159161
Keymap *keymap.Keymap // custom key bindings (nil uses defaults)
@@ -193,6 +195,7 @@ func NewModel(renderer Renderer, store *annotation.Store, highlighter SyntaxHigh
193195
wrapMode: cfg.Wrap,
194196
lineNumbers: cfg.LineNumbers,
195197
collapsed: collapsedState{enabled: cfg.Collapsed},
198+
showBlame: cfg.ShowBlame && cfg.Blamer != nil,
196199
focus: paneTree,
197200
treeWidthRatio: cfg.TreeWidthRatio,
198201
tabSpaces: strings.Repeat(" ", cfg.TabWidth),
@@ -1235,7 +1238,7 @@ func (m Model) statusModeIcons() string {
12351238
{"≋", len(m.searchMatches) > 0},
12361239
{"⊟", m.treeHidden},
12371240
{"#", m.lineNumbers},
1238-
{"@", m.showBlame},
1241+
{"b", m.showBlame},
12391242
}
12401243

12411244
statusFg := m.styles.colors.Muted

ui/model_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,33 @@ func TestModel_LineNumbersFromConfig(t *testing.T) {
452452
})
453453
}
454454

455+
func TestModel_BlameFromConfig(t *testing.T) {
456+
renderer := &mocks.RendererMock{
457+
ChangedFilesFunc: func(string, bool) ([]string, error) { return nil, nil },
458+
FileDiffFunc: func(string, string, bool) ([]diff.DiffLine, error) { return nil, nil },
459+
}
460+
store := annotation.NewStore()
461+
blamer := &mocks.BlamerMock{
462+
FileBlameFunc: func(string, string, bool) (map[int]diff.BlameLine, error) { return map[int]diff.BlameLine{}, nil },
463+
}
464+
465+
t.Run("blame enabled via config when blamer is available", func(t *testing.T) {
466+
m := NewModel(renderer, store, noopHighlighter(), ModelConfig{ShowBlame: true, Blamer: blamer, TreeWidthRatio: 2})
467+
assert.True(t, m.showBlame)
468+
})
469+
470+
t.Run("blame disabled without blamer even if requested", func(t *testing.T) {
471+
m := NewModel(renderer, store, noopHighlighter(), ModelConfig{ShowBlame: true, TreeWidthRatio: 2})
472+
assert.False(t, m.showBlame)
473+
})
474+
475+
t.Run("blame disabled by default", func(t *testing.T) {
476+
m := NewModel(renderer, store, noopHighlighter(), ModelConfig{Blamer: blamer, TreeWidthRatio: 2})
477+
assert.False(t, m.showBlame)
478+
})
479+
}
480+
481+
455482
func TestModel_StatusModeIcons(t *testing.T) {
456483
t.Run("all icons always present", func(t *testing.T) {
457484
m := testModel(nil, nil)
@@ -6970,6 +6997,14 @@ func TestModel_StatusModeIconsLineNumbers(t *testing.T) {
69706997
assert.Contains(t, icons, "#")
69716998
}
69726999

7000+
func TestModel_StatusModeIconsBlame(t *testing.T) {
7001+
m := testModel(nil, nil)
7002+
m.showBlame = true
7003+
icons := m.statusModeIcons()
7004+
assert.Contains(t, icons, "b")
7005+
assert.NotContains(t, icons, "@")
7006+
}
7007+
69737008
func TestModel_HelpOverlayContainsLineNumbers(t *testing.T) {
69747009
m := testModel(nil, nil)
69757010
m.width = 120

0 commit comments

Comments
 (0)