Skip to content

git worktree repair is routed to list mode: prints the worktree list, exits 0, repairs nothing #3710

Description

@PascalThuet

Summary

run_worktree's has_action set is missing repair, so git worktree repair <path> falls through to list mode. rtk runs git worktree list instead, prints the filtered list, and returns 0. The repair never runs and nothing says so.

Same masking class as #2497, but the cause is different: there the read path swallowed a git failure, here a write subcommand never reaches git at all.

Found through the Claude Code PreToolUse hook, which rewrites git worktree repair ... to rtk git worktree repair .... The agent read the list output as a successful repair and moved on with two still-broken worktrees.

Reproduce

$ git init -q A && cd A && git commit -q --allow-empty -m init
$ git worktree add -q -b wt A/.worktrees/wt
$ cd .. && mv A B && cd B          # moving the repo breaks both gitdir pointers

$ cat B/.worktrees/wt/.git
gitdir: /abs/path/A/.git/worktrees/wt          # stale, points at the old location

$ rtk git worktree repair /abs/path/B/.worktrees/wt
/abs/path/B                       9d3c6f8 [main]
/abs/path/A/.worktrees/wt         9d3c6f8 [wt] prunable
$ echo $?
0
$ cat B/.worktrees/wt/.git
gitdir: /abs/path/A/.git/worktrees/wt          # unchanged

$ git worktree repair /abs/path/B/.worktrees/wt
repair: gitdir incorrect: /abs/path/B/.git/worktrees/wt/gitdir
$ cat B/.worktrees/wt/.git
gitdir: /abs/path/B/.git/worktrees/wt          # repaired

rtk 0.45.0, macOS.

Cause

src/cmds/git/git.rs:2143

let has_action = args.iter().any(|a| {
    a == "add" || a == "remove" || a == "prune" || a == "lock" || a == "unlock" || a == "move"
});

git has eight worktree subcommands: add, list, lock, move, prune, remove, repair, unlock. Six are listed, list is the fallback, repair is the only one missing.

I verified the other six empirically: add, remove, remove --force, lock, unlock, move and prune all take effect. repair is the only broken one.

Two more defects at the same site

List mode drops the caller's flags. Line 2179 hardcodes the invocation:

cmd.args(["worktree", "list"]);

So git worktree list --porcelain returns the compact filtered format, not porcelain. Anything parsing that output gets a shape it never asked for:

$ rtk git worktree list --porcelain
/abs/path/B    9d3c6f8 [main]        # not porcelain

This is arguably worse than the repair case, since porcelain exists precisely to be machine-read.

has_action matches the token in any position. args.iter().any(...) tests every argument, not the subcommand slot. A worktree whose directory is named move or remove selects the action branch regardless of the actual subcommand.

Fix shape

  • Add repair to the has_action set, or invert it: treat everything except list (and no subcommand) as pass-through, so the next git release does not reopen this.
  • In list mode, forward the caller's arguments instead of hardcoding ["worktree", "list"], and skip filtering when a format flag such as --porcelain or -z is present.
  • Match the subcommand at args[0] rather than anywhere in args.

Workaround

exclude_commands = ["git worktree"] in the rtk config keeps the rest of the git proxying while removing the hazard.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions