Skip to content

Fix five defects in git-trees#36

Merged
leogdion merged 3 commits into
v1.0.0-integrationfrom
script-bugs
Jul 25, 2026
Merged

Fix five defects in git-trees#36
leogdion merged 3 commits into
v1.0.0-integrationfrom
script-bugs

Conversation

@leogdion

@leogdion leogdion commented Jul 25, 2026

Copy link
Copy Markdown
Member

Merge order: 2 of 6, into v1.0.0-integration. Cut from the same commit as
main, so it still contains clean; expect a ci.yml conflict with #35, which
deletes that command.

Closes #18, closes #22, closes #23, closes #31, closes #32.

Five independent defects, all in git-trees, in disjoint hunks.

#18 — missing option value hangs forever

shift 2 with one argument left fails and leaves $1 in place, so
while [ $# -gt 0 ] spins. "${2:-}" defends against set -u, which is why it
reads as safe — the bug is the shift, not the expansion. Both --host and
--dir now check arity first.

clean --older-than has the identical defect. It is not fixed here — that
command is deleted by #35. Merging this PR without #35 leaves that one hang in
place.

#22init printed success after post-clone steps failed

The refspec and fetch are what give a bare clone its remote-tracking refs;
without them the container has no origin/* and add cannot resolve the
default branch. Now chained, with the same rm -rf "$dir" rollback as the clone
path — so a retry isn't blocked by init: $dir already exists.
set-head --auto stays unchecked; it fails harmlessly against a remote with no
HEAD.

#23list --json invalid for paths containing \

Only " was escaped, so \s reached the output and strict parsers rejected the
document. Fixed with a character loop rather than gsub:

awk reads "\\" in a replacement string as one literal backslash, so the
obvious gsub(/\\/, "\\\\", s) under-escapes. Plain concatenation has no such
rule.

Control characters now map to \u00XX.

#31add reported the wrong error for invalid names

add .. resolved to $root/.., which always exists, so it failed with a
directory collision; add 'has space' failed only after worktree add had
printed Preparing worktree…. Names are validated with git check-ref-format
before anything else touches them.

git check-ref-format "refs/heads/$br" — not --branch, which expands @{-1}
and needs a repo.

Leading-dash names stay unsupported (parsed as options) and are now documented
under Known limitations.

#32_seed_agents operator precedence

[ -e f ] || [ -L f ] && return 0 groups as { A || B; } && return 0, so the
compound status is 1 when neither test holds. Both guards are explicit if
blocks now; both tests are kept, since -e is false for a broken symlink while
-L is true. A genuine write failure now reports why instead of a bare 1.

Verified

bash -n and shellcheck -s bash clean. Against file:// fixtures:

Check Result
init --host / --dir / org/repo --host, no value exit 1 promptly with a named error — no hang
init --dir okdir (valid) still exits 0
Simulated post-clone fetch failure exit 1, no "initialized", directory removed, retry succeeds
add . .. 'has space' tilde~x ends.lock a..b all exit 1, none reach Preparing worktree, none report a collision
add feature/x unchanged slash message
Worktree at back\slash "quoted" old script → JSONDecodeError: Invalid \escape; new → parses, path round-trips byte-exact
_seed_agents: absent / regular file / broken symlink / unwritable dir seeds / leaves MINE / leaves the link / exits 1 with a message

Both AGENTS.md regressions re-checked: add feature-xorigin/feature-x,
add brandneworigin/brandnew. The full smoke block was run locally end to
end before pushing.

🤖 Generated with Claude Code

**#18 — `init --host` / `--dir` with no value hang forever.** `shift 2` with one
argument left fails and leaves `$1` in place, so `while [ $# -gt 0 ]` spins.
`"${2:-}"` defends against `set -u`, which is why it read as safe; the bug is
the shift. Both options now check arity first. (`clean --older-than` has the
same defect and is fixed by that command's removal in #17.)

**#22 — `init` printed success after post-clone steps failed.** The refspec and
fetch are what give a bare clone its remote-tracking refs; without them `add`
cannot resolve the default branch. They are now chained and roll back the
directory on failure, matching the clone path, so a retry is not blocked by
"already exists". `set-head --auto` stays unchecked — it fails harmlessly
against a remote with no HEAD.

**#23 — `list --json` emitted invalid JSON for a path containing a backslash.**
Only `"` was escaped. Escaping is now done character by character: awk reads
`"\\"` in a *replacement* string as one literal backslash, so the obvious
`gsub(/\\/, "\\\\", s)` under-escapes. Control characters map to `\u00XX`.

**#31 — `add` reported the wrong error for `.`, `..`, and other invalid names.**
`add ..` resolved to `$root/..`, which always exists, so it failed with a
directory collision; `add 'has space'` failed only after `worktree add` printed
"Preparing worktree…". Names are now validated with `git check-ref-format`
before anything else touches them. Leading-dash names remain unsupported and
are documented under Known limitations.

**#32 — `_seed_agents` operator-precedence guard.** `[ -e f ] || [ -L f ] &&
return 0` groups as `{ A || B; } && return 0`, so the compound status is 1 when
neither test holds. Both guards are now explicit `if` blocks, and a genuine
write failure reports why instead of returning bare 1.

CI gains assertions for each: the missing-value cases bounded so a regression
fails instead of hanging the runner, `add .` / `..` / `'has space'`, and a
`list --json` round-trip through a path containing `\` and `"`.

Closes #18, closes #22, closes #23, closes #31, closes #32.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d97c1bb-8b70-4b16-8a38-fed01c100e8f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch script-bugs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@leogdion leogdion left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens when an existing branch has a slash

`add feature/x` was rejected outright by a `*/*` guard that ran before any
existence check, so a branch already present locally or on origin could never
get a worktree — and the error told you to rename it. `list` meanwhile
enumerated refs/heads unfiltered and showed `feature/x` with path `(none)`,
advertising branches `add` refused to serve.

The restriction was only ever about the directory name: every worktree is a
direct child of the container root, so a branch's `/` cannot be a directory
separator. Slug it to `-` instead and leave the branch name alone.

`feature/x` and `feature-x` now compete for one directory. `_branch_at` reports
which branch owns it so the collision error says what to rename, rather than
inventing a suffixed variant the user cannot predict.

`git check-ref-format` becomes the sole name validator — it already accepts
`feature/x` and rejects `.`, `..`, `has space`, and `a//b`. Silenced, since it
runs ahead of the `--print-path` branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@leogdion

Copy link
Copy Markdown
Member Author

what happens when an existing branch has a slash

It was rejected outright, and the error was misleading. The */* guard in cmd_add ran before any existence check, so git trees add feature/x printed "use dashes (e.g. feature-x), not feature/x" even when feature/x already existed locally or on origin — advice you can't act on for a branch you didn't name. cmd_list meanwhile enumerated refs/heads unfiltered and happily showed feature/x with path (none), so list advertised branches add refused to serve. track handled them fine.

Fixed in f0e28af by supporting them. The restriction was only ever about the directory name — worktrees are direct children of the container root and can't nest — so the directory now slugs / to - while the branch keeps its real name:

git trees add feature/x   →   feature-x/   branch feature/x   upstream origin/feature/x
git trees add a/b/c       →   a-b-c/

The consequence is that feature/x and feature-x compete for one directory. Rather than invent a suffixed variant nobody can predict, add refuses the second and names the branch that owns it:

git trees add: 'feature-x' and 'feature/x' both map to /path/to/proj/feature-x — rename one

git check-ref-format is now the sole name validator; it already accepts feature/x and rejects ., .., has space, and a//b, all verified against a real repo. CI gained a pre-existing slug/one branch on the origin fixture plus three assertions (slug path, collision naming, and a new multi-component branch that must not inherit origin/main). Full smoke suite passes locally.

@leogdion
leogdion merged commit 15bb7df into v1.0.0-integration Jul 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant