Skip to content

Commit 93e5ec5

Browse files
wesmclaude
andauthored
Fix hook v1→v2 upgrade: strip & and document install-hook --force (#207)
## Summary - Fix the v1→v2 hook upgrade path to patch in place: strips trailing `&` from the enqueue line and adds the v2 version marker - For hooks already corrupted by a previous buggy upgrade (e.g. stray `fi`), the recommended fix is `roborev install-hook --force` - Remove the `removeStrayFi()` auto-repair logic in favor of the simpler `--force` approach ## Test plan - [x] `TestInitCmdUpgradesOutdatedHook` — v1 hook gets `&` stripped and marker added - [x] `TestInitCmdPreservesOtherHooksOnUpgrade` — non-roborev hook content preserved during upgrade - [x] Full test suite passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2020137 commit 93e5ec5

File tree

2 files changed

+24
-27
lines changed

2 files changed

+24
-27
lines changed

cmd/roborev/hook_test.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,8 @@ func TestInitCmdUpgradesOutdatedHook(t *testing.T) {
309309

310310
repo := testutil.NewTestRepo(t)
311311

312-
// Write an old-style hook (no version marker, has &)
313-
oldHook := "#!/bin/sh\n# roborev post-commit hook - auto-reviews every commit\nROBOREV=\"/usr/local/bin/roborev\"\n\"$ROBOREV\" enqueue --quiet 2>/dev/null &\n"
312+
// Write a realistic old-style hook (no version marker, has &, includes if/fi block)
313+
oldHook := "#!/bin/sh\n# roborev post-commit hook - auto-reviews every commit\nROBOREV=\"/usr/local/bin/roborev\"\nif [ ! -x \"$ROBOREV\" ]; then\n ROBOREV=$(command -v roborev 2>/dev/null)\n [ -z \"$ROBOREV\" ] || [ ! -x \"$ROBOREV\" ] && exit 0\nfi\n\"$ROBOREV\" enqueue --quiet 2>/dev/null &\n"
314314
repo.WriteHook(oldHook)
315315

316316
defer testutil.MockBinaryInPath(t, "roborev", "#!/bin/sh\nexit 0\n")()
@@ -335,6 +335,13 @@ func TestInitCmdUpgradesOutdatedHook(t *testing.T) {
335335
if strings.Contains(contentStr, "2>/dev/null &") {
336336
t.Error("upgraded hook should not have backgrounded enqueue")
337337
}
338+
if !strings.Contains(contentStr, "2>/dev/null\n") {
339+
t.Error("upgraded hook should still have enqueue line (without &)")
340+
}
341+
// Verify the if/fi block is preserved intact (no stray fi)
342+
if !strings.Contains(contentStr, "if [ ! -x") {
343+
t.Error("upgraded hook should preserve the if block")
344+
}
338345
}
339346

340347
func TestInitCmdPreservesOtherHooksOnUpgrade(t *testing.T) {
@@ -349,7 +356,7 @@ func TestInitCmdPreservesOtherHooksOnUpgrade(t *testing.T) {
349356

350357
repo := testutil.NewTestRepo(t)
351358

352-
oldHook := "#!/bin/sh\necho 'my custom hook'\n# roborev post-commit hook - auto-reviews every commit\nROBOREV=\"/usr/local/bin/roborev\"\n\"$ROBOREV\" enqueue --quiet 2>/dev/null &\n"
359+
oldHook := "#!/bin/sh\necho 'my custom hook'\n# roborev post-commit hook - auto-reviews every commit\nROBOREV=\"/usr/local/bin/roborev\"\nif [ ! -x \"$ROBOREV\" ]; then\n ROBOREV=$(command -v roborev 2>/dev/null)\n [ -z \"$ROBOREV\" ] || [ ! -x \"$ROBOREV\" ] && exit 0\nfi\n\"$ROBOREV\" enqueue --quiet 2>/dev/null &\n"
353360
repo.WriteHook(oldHook)
354361

355362
defer testutil.MockBinaryInPath(t, "roborev", "#!/bin/sh\nexit 0\n")()
@@ -374,6 +381,9 @@ func TestInitCmdPreservesOtherHooksOnUpgrade(t *testing.T) {
374381
if !strings.Contains(contentStr, "hook v2") {
375382
t.Error("upgrade should contain version marker")
376383
}
384+
if strings.Contains(contentStr, "2>/dev/null &") {
385+
t.Error("upgrade should remove trailing &")
386+
}
377387
}
378388

379389
func TestHookNeedsUpgrade(t *testing.T) {

cmd/roborev/main.go

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -346,29 +346,19 @@ func initCmd() *cobra.Command {
346346
fmt.Println(" Hook already installed")
347347
goto startDaemon
348348
} else {
349-
// Upgrade: strip old roborev lines, append new content
350-
lines := strings.Split(existingStr, "\n")
351-
var kept []string
352-
for _, line := range lines {
353-
if !strings.Contains(strings.ToLower(line), "roborev") {
354-
kept = append(kept, line)
349+
// Upgrade: try patching in place first (preserves hook structure)
350+
// v1 → v2: remove trailing & from enqueue line, add version marker
351+
upgraded := existingStr
352+
upgraded = strings.Replace(upgraded, "2>/dev/null &", "2>/dev/null", 1)
353+
upgraded = strings.Replace(upgraded, "post-commit hook -", "post-commit hook v2 -", 1)
354+
if !strings.Contains(upgraded, hookVersionMarker) {
355+
// Comment was edited — just append a marker so we don't re-upgrade
356+
if !strings.HasSuffix(upgraded, "\n") {
357+
upgraded += "\n"
355358
}
359+
upgraded += "# " + hookVersionMarker + "\n"
356360
}
357-
// Remove trailing empty lines
358-
for len(kept) > 0 && strings.TrimSpace(kept[len(kept)-1]) == "" {
359-
kept = kept[:len(kept)-1]
360-
}
361-
// Strip shebang from new content if prepending to existing content
362-
newHook := hookContent
363-
if len(kept) > 0 {
364-
newHook = strings.TrimPrefix(newHook, "#!/bin/sh\n")
365-
}
366-
if len(kept) > 0 {
367-
hookContent = strings.Join(kept, "\n") + "\n" + newHook
368-
} else {
369-
hookContent = newHook
370-
}
371-
if err := os.WriteFile(hookPath, []byte(hookContent), 0755); err != nil {
361+
if err := os.WriteFile(hookPath, []byte(upgraded), 0755); err != nil {
372362
return fmt.Errorf("upgrade hook: %w", err)
373363
}
374364
fmt.Println(" Upgraded post-commit hook")
@@ -2839,9 +2829,6 @@ func hookNeedsUpgrade(repoPath string) bool {
28392829
return strings.Contains(strings.ToLower(s), "roborev") && !strings.Contains(s, hookVersionMarker)
28402830
}
28412831

2842-
// generateHookContent creates the post-commit hook script content.
2843-
// It bakes the path to the currently running binary for consistency.
2844-
// Falls back to PATH lookup if the baked path becomes unavailable.
28452832
func generateHookContent() string {
28462833
// Get path to the currently running binary (not just first in PATH)
28472834
roborevPath, err := os.Executable()

0 commit comments

Comments
 (0)