Skip to content

feat(clone): add wt clone with repo_root + repo_pattern placement - #127

Merged
timvw merged 6 commits into
mainfrom
feat/clone-simple
Aug 19, 2026
Merged

feat(clone): add wt clone with repo_root + repo_pattern placement#127
timvw merged 6 commits into
mainfrom
feat/clone-simple

Conversation

@timvw

@timvw timvw commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Adapts @brosu's wt clone from #122 (their commits are preserved here) to a smaller configuration surface.

What changed vs #122

#122 introduced a category tier: a [categories.*] registry with built-in work/personal/oss, each carrying a repo_root and an auth profile (gh_auth, git_protocol, glab_host), plus a default_category. This drops that tier and keeps two flat settings:

repo_root    = "~/dev/repos"
repo_pattern = "{.repoRoot}/{.repo.Host}/{.repo.Owner}/{.repo.Name}/{.branch}"

wt clone timvw/wt~/dev/repos/github.com/timvw/wt/main.

Grouping is no longer a concept wt owns. Patterns already support {.env.VARNAME}, so anyone who wants categories writes one:

repo_pattern = "{.repoRoot}/{.env.WT_CATEGORY}/{.repo.Owner}/{.repo.Name}/{.branch}"
WT_CATEGORY=work wt clone acme/api   # ~/dev/repos/work/acme/api/main

That covers category, client, year, or any other axis, without wt picking the vocabulary — and it composes with the worktree pattern mechanism users already know.

Auth follows the same principle. #122 called gh auth switch per category; that mutates gh's global hosts.yml and silently flips the active account for every other process on the machine, including ones the user did not run. Account selection stays with the tools that own it (GH_CONFIG_DIR, GH_TOKEN, GLAB_HOST), and each of gh/glab is now asked for its own git_protocol instead of gh deciding for both.

Fixes on top of the original implementation

  • Relative patterns: a repo_pattern without {.repoRoot} cloned into the caller's current directory. Now anchored under repo_root, matching how renderWorktreePath anchors relative worktree patterns.
  • Path traversal: .. in the rendered host, owner, repo name or branch is rejected. wt clone https://host/../../tmp/pwn.git — or an scp-like ../escape:owner/repo.git, where the traversal is the host — placed the clone outside repo_root. A pasted URL is untrusted input.
  • Argument injection: -- before the URL in git clone and git ls-remote, so a source starting with - cannot be read as an option.
  • Clone hook env: WT_MAIN and WT_BRANCH were both empty. The clone is its own main worktree, and the default branch is now resolved once up front and shared by placement and the hook env.
  • Repo-level config: repo_root, repo_pattern and the clone hooks are not read from a repo's .wt.toml. wt clone acquires a repository unrelated to the one you are standing in, so that repo's config must not redirect the destination or run hooks — mirroring the existing exclusion of root.
  • Path anchoring: keyed on whether the pattern names {.repoRoot} rather than on filepath.IsAbs. A relative repo_root, or a rooted-but-driveless Windows one (\data\repos), is not IsAbs, so the root was being prepended a second time.
  • Local paths with a colon: parseRemoteURL read any string containing : as scp-like, so on Windows wt clone C:/src/repo parsed host C, owner Users/... and placed the clone at <repo_root>/C/Users/.../repo/main instead of reporting it cannot derive a placement. Now follows git's rules — the colon must precede any slash, and a single character before it is a drive letter. Real remotes are unaffected.
  • config show sizes its value column to its contents, so the long default repo_pattern keeps its source marker aligned.

Why the trailing {.branch}

The clone lands at <repo>/<default-branch> rather than <repo>. That makes it a normal worktree slot, so a later wt create feat/x puts a sibling next to it instead of nesting inside the clone. Users who prefer a bare <owner>/<repo> checkout drop the segment from the pattern.

Testing

  • go test ./..., go vet ./..., gofmt clean
  • e2e green on bash + zsh (clone and hooks scenarios; new clone_hook_env_has_branch verifies WT_BRANCH, and it fails if the variable is empty)
  • New unit tests for placement, the relative-pattern anchor, the {.env.X} grouping mechanism, and the traversal guard
  • One pre-existing e2e failure (config/config_show_defaults) reproduces on main with an unmodified binary and is unrelated

Closes #122.

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 37.09677% with 156 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.10%. Comparing base (2867d8d) to head (258fc57).

Files with missing lines Patch % Lines
cmd/clone.go 9.25% 98 Missing ⚠️
internal/tmpl/tmpl.go 0.00% 23 Missing ⚠️
cmd/clone_path.go 65.11% 13 Missing and 2 partials ⚠️
cmd/info.go 0.00% 13 Missing ⚠️
cmd/config.go 70.00% 4 Missing and 2 partials ⚠️
cmd/shellenv.go 83.33% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #127      +/-   ##
==========================================
- Coverage   37.53%   37.10%   -0.43%     
==========================================
  Files          30       33       +3     
  Lines        3229     3444     +215     
==========================================
+ Hits         1212     1278      +66     
- Misses       1928     2073     +145     
- Partials       89       93       +4     
Files with missing lines Coverage Δ
cmd/config_cmd.go 65.62% <100.00%> (+7.13%) ⬆️
cmd/examples.go 67.74% <ø> (ø)
cmd/hooks.go 89.74% <100.00%> (+0.55%) ⬆️
cmd/repo.go 65.97% <100.00%> (+1.81%) ⬆️
cmd/root.go 70.78% <100.00%> (+0.33%) ⬆️
cmd/worktree_path.go 56.00% <100.00%> (-2.54%) ⬇️
cmd/shellenv.go 79.05% <83.33%> (+0.23%) ⬆️
cmd/config.go 69.93% <70.00%> (-0.87%) ⬇️
cmd/info.go 0.00% <0.00%> (ø)
cmd/clone_path.go 65.11% <65.11%> (ø)
... and 2 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

brosu and others added 6 commits August 19, 2026 14:59
Introduce `wt clone <category> <owner/repo|url> [dest]`: acquire a repo's
canonical checkout under a category's repo_root in a host/owner/repo layout,
left on its default branch and ready to inspect. Fills wt's missing front
half — every other command assumes the repo already exists locally.

Adds the `categories` config concept (repo_root + gh_auth / git_protocol /
glab_host) that drives placement and auth. owner/repo is resolved to a clone
URL via gh/glab honoring git_protocol; a full URL is used as-is.

- cmd/clone.go: command, URL resolution, gh auth switch, placement, hooks
- cmd/category.go: Category type, builtins, resolve/merge, repoPlacementPath
- cmd/config.go: [categories.*], default_category, repo_pattern, clone hooks
- README/llms.txt/examples + unit tests + e2e scenarios

Default repo_pattern now places clones at owner/repo/<branch> where
<branch> is the remote's default branch, resolved via git ls-remote
--symref before cloning. This makes the clone directory a valid
main-worktree slot for sibling worktree strategies.

Falls back to "main" when the remote is unreachable.
Reduces the clone configuration to two flat settings instead of a
category registry with per-category auth and roots.

Placement is now repo_root + repo_pattern, defaulting to
{.repoRoot}/{.repo.Host}/{.repo.Owner}/{.repo.Name}/{.branch}. Grouping
levels such as work/personal/oss are no longer a built-in concept: a
pattern can reference {.env.VARNAME}, so users express whatever axis they
care about (category, client, year) without wt owning the vocabulary.

Auth follows the same principle. wt no longer runs `gh auth switch`,
which mutates gh's global hosts.yml and silently flips the active account
for every other process on the machine. Account selection stays with the
tools that own it (GH_CONFIG_DIR, GH_TOKEN, GLAB_HOST), and each of
gh/glab is asked for its own git_protocol rather than gh deciding for
both.

Other fixes on top of the original implementation:

- Anchor a repo_pattern that omits {.repoRoot} under repo_root, instead
  of cloning into the caller's current directory.
- Reject a ".." path component in the host, owner, repo name or branch
  rendered into the placement path. A pasted URL such as
  https://host/../../tmp/pwn.git, or an scp-like ../escape:owner/repo.git
  whose host is the traversal, would otherwise clone outside repo_root.
- Pass "--" to git clone and git ls-remote so a source beginning with "-"
  cannot be read as an option.
- Set WT_MAIN and WT_BRANCH for clone hooks; both were empty. The clone
  is its own main worktree, and the default branch is now resolved once
  up front and shared by placement and the hook environment.
- Do not read repo_root, repo_pattern or clone hooks from a repo-level
  .wt.toml. clone targets a repository other than the one you are
  standing in, so that repo's config must not redirect the destination
  or run hooks. Mirrors the existing exclusion of `root`.
- Size the `config show` value column to its contents so long patterns
  keep their source marker aligned.

Co-authored-by: brosu <bogdanrosu24@gmail.com>
…aths

filepath.IsAbs is false for a rooted-but-driveless Windows path
("\data\repos") and for a relative repo_root, so a pattern that already
rendered {.repoRoot} was anchored under the repo root a second time,
producing <root>/<root>/host/owner/repo. Key the anchoring on whether the
pattern names {.repoRoot} instead: if it does, the path is already
anchored however repo_root happens to be spelled.

The placement tests hard-coded "/tmp/repos", which is not absolute on
Windows and made them fail there for the same reason. They now use
t.TempDir(), and a new case covers the double-anchor regression directly.
fish and POSIX shells disagree on backslash handling inside single quotes,
so the escaped \" in the printf that writes the config produced invalid
TOML under fish. Use "test x$WT_BRANCH != x" and a marker file instead,
matching the quoting style of the neighbouring hook scenarios. Verified
still non-vacuous: an empty variable leaves the marker absent.
parseRemoteURL treated any string containing ":" as an scp-like remote,
so on Windows a local source such as C:/Users/runner/repo parsed as
host "C", owner "Users/runner", name "repo" — and `wt clone C:/src/repo`
placed the clone at <repo_root>/C/Users/runner/repo/main instead of
reporting that it cannot derive a placement.

Follow git's own rules instead: the colon must precede any slash, and a
single character before it is a drive letter rather than a host. Real
remotes (git@host:owner/repo.git, host:group/sub/repo.git) are unaffected.

This is what the clone/clone_local_path_without_dest_fails e2e scenario
was catching on windows+bash.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants