feat(clone): add wt clone with repo_root + repo_pattern placement - #127
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ 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
🚀 New features to boost your workflow:
|
This was referenced Aug 19, 2026
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.
timvw
force-pushed
the
feat/clone-simple
branch
from
August 19, 2026 12:59
917a1c4 to
258fc57
Compare
This was referenced Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adapts @brosu's
wt clonefrom #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-inwork/personal/oss, each carrying arepo_rootand an auth profile (gh_auth,git_protocol,glab_host), plus adefault_category. This drops that tier and keeps two flat settings: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:WT_CATEGORY=work wt clone acme/api # ~/dev/repos/work/acme/api/mainThat 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 switchper category; that mutates gh's globalhosts.ymland 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 ofgh/glabis now asked for its owngit_protocolinstead of gh deciding for both.Fixes on top of the original implementation
repo_patternwithout{.repoRoot}cloned into the caller's current directory. Now anchored underrepo_root, matching howrenderWorktreePathanchors relative worktree patterns...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 outsiderepo_root. A pasted URL is untrusted input.--before the URL ingit cloneandgit ls-remote, so a source starting with-cannot be read as an option.WT_MAINandWT_BRANCHwere 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_root,repo_patternand the clone hooks are not read from a repo's.wt.toml.wt cloneacquires 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 ofroot.{.repoRoot}rather than onfilepath.IsAbs. A relativerepo_root, or a rooted-but-driveless Windows one (\data\repos), is notIsAbs, so the root was being prepended a second time.parseRemoteURLread any string containing:as scp-like, so on Windowswt clone C:/src/repoparsed hostC, ownerUsers/...and placed the clone at<repo_root>/C/Users/.../repo/maininstead 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 showsizes its value column to its contents, so the long defaultrepo_patternkeeps 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 laterwt create feat/xputs 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 ./...,gofmtcleancloneandhooksscenarios; newclone_hook_env_has_branchverifiesWT_BRANCH, and it fails if the variable is empty){.env.X}grouping mechanism, and the traversal guardconfig/config_show_defaults) reproduces onmainwith an unmodified binary and is unrelatedCloses #122.