Skip to content

feat(clone): add wt clone with category-based placement - #122

Closed
brosu wants to merge 2 commits into
timvw:mainfrom
brosu:feat/clone
Closed

feat(clone): add wt clone with category-based placement#122
brosu wants to merge 2 commits into
timvw:mainfrom
brosu:feat/clone

Conversation

@brosu

@brosu brosu commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Motivation

Every wt command assumes you are already inside a repo. There is no way to acquire the main repository in the first place.
wt clone fills that gap.

What this adds

wt clone <owner/repo|url> [dest] — clones a repository into its canonical location for a category, left on its
default branch, ready to inspect. A worktree can be added later with the usual wt commands.

wt clone oss timvw/wt # -> ~/dev/repos/oss/timvw/wt/main
wt clone work owner/repo # -> ~/dev/repos/work/owner/repo/main
wt clone personal git@…/x.git # full URL, same layout
wt clone personal git@…/x.git ~/src/x # explicit dest, bypasses layout

Categories

A category is an organizational context: where repos live (repo_root) plus the auth profile used to reach them (gh_auth,
git_protocol, glab_host). Three builtins ship: work, personal, oss.

repo_root = "~/dev/repos" # base; category root = <repo_root>/

[categories.work]
gh_auth = "work"
git_protocol = "ssh"

[categories.personal]
gh_auth = "personal"

[categories.client]
repo_root = "~/clients/acme/src" # explicit override
glab_host = "gitlab.acme.com"

Placement pattern

Default: {.category.RepoRoot}/{.repo.Owner}/{.repo.Name}/{.branch}

{.branch} is the remote's default branch, resolved via git ls-remote --symref before cloning (fallback "main"). This makes
the clone directory a valid main-worktree slot for sibling-worktree strategies. Override with repo_pattern.

Other details

  • owner/repo shorthand resolved via gh (or glab), honouring git_protocol
  • gh auth switch --user <gh_auth> before resolve/clone (best-effort, warns on failure)
  • pre_clone / post_clone hooks (pre aborts on failure, post warns only)
  • --format json output with navigate_to
  • Shell completions for bash/zsh/fish/PowerShell (category arg completes work personal oss)
  • wt info and wt examples clone updated
  • Extracts shared template rendering into internal/tmpl (used by worktree_path.go and category.go)

Tests

  • Unit: cmd/clone_test.go — placement path, URL passthrough, category resolution, owner/repo detection
  • e2e: e2e/scenarios/clone.yaml — local-repo clone, unknown category, explicit dest, hooks

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.
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 43.76731% with 203 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.85%. Comparing base (b29508f) to head (dddf5a6).

Files with missing lines Patch % Lines
cmd/clone.go 7.69% 120 Missing ⚠️
internal/tmpl/tmpl.go 0.00% 23 Missing ⚠️
cmd/category.go 75.00% 14 Missing and 6 partials ⚠️
cmd/info.go 0.00% 17 Missing ⚠️
cmd/config.go 60.97% 10 Missing and 6 partials ⚠️
cmd/shellenv.go 78.26% 5 Missing ⚠️
cmd/config_cmd.go 94.73% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #122      +/-   ##
==========================================
+ Coverage   37.62%   37.85%   +0.23%     
==========================================
  Files          28       31       +3     
  Lines        3216     3545     +329     
==========================================
+ Hits         1210     1342     +132     
- Misses       1917     2101     +184     
- Partials       89      102      +13     
Files with missing lines Coverage Δ
cmd/examples.go 67.74% <ø> (ø)
cmd/hooks.go 89.74% <100.00%> (+0.55%) ⬆️
cmd/root.go 70.78% <100.00%> (+0.33%) ⬆️
cmd/worktree_path.go 56.00% <100.00%> (-2.54%) ⬇️
cmd/config_cmd.go 71.08% <94.73%> (+12.59%) ⬆️
cmd/shellenv.go 78.82% <78.26%> (+0.12%) ⬆️
cmd/config.go 67.81% <60.97%> (-2.99%) ⬇️
cmd/info.go 0.00% <0.00%> (ø)
cmd/category.go 75.00% <75.00%> (ø)
internal/tmpl/tmpl.go 0.00% <0.00%> (ø)
... and 1 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@timvw

timvw commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Let me think about this (Original thinking was to focus on worktrees (only)... replicating git clone/init was intentionally not considered...)

@brosu

brosu commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Sure, take your time; I would see workspace management as a future enhancement as well.
The examples for one feature multiple repos gave me some ideas.
Of course, all that would extend the tool's responsibilities.

@timvw

timvw commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Thanks for this @brosu — the core idea was right and the implementation was solid. wt clone shipped in #127, which builds directly on your two commits (they're preserved in the branch with your authorship, and you're credited as co-author on the merge).

What I kept: the whole shape of the command — acquiring the main repo, owner/repo resolution through gh/glab, {.branch} resolved via git ls-remote --symref so the clone lands in a valid main-worktree slot, pre_clone/post_clone hooks, JSON output with navigate_to, completions, and the internal/tmpl extraction. That last one turned out to be the most useful piece: it's what makes the rest of this work.

What I changed: the category tier is gone. Placement is now two flat settings:

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

Two reasons. First, [categories.*] made wt own a vocabulary (work/personal/oss) that only fits some people — the axis is a client for one user, a year for another. Since patterns already support {.env.VARNAME}, the grouping level composes without wt naming it:

repo_pattern = "{.repoRoot}/{.env.WT_CATEGORY}/{.repo.Owner}/{.repo.Name}/{.branch}"

Second, gh auth switch --user mutates gh's global hosts.yml. That flips the active account for every other process on the machine, including ones the user didn't run and won't connect to wt. 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 rather than gh deciding for both.

A few bugs got fixed along the way, mostly in code your PR didn't introduce: a repo_pattern without {.repoRoot} cloned into the caller's cwd; .. in a pasted URL's host or path placed the clone outside repo_root; WT_MAIN and WT_BRANCH were empty in clone hooks; and parseRemoteURL read C:/src/repo as an scp-like remote with host C.

On categories — you were solving a real problem and I don't want that to get lost. {.env.WT_CATEGORY} is a weaker answer than what you proposed: it hard-fails when the variable isn't exported (missingkey=error), and it's ephemeral — there's no way to record "this repo is work" durably. Your [categories.*] had an answer to both.

That's now #132, which tracks the real requirement: user-owned config that spans a group of repos and is never committed. Directory-scoped .wt.toml discovery outside the repo, a generic vars layer, git config (#125/#128) — your PR is cited there as one of the three threads that converged on it. Your input on that issue would be welcome, since you hit the problem first.

@timvw

timvw commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Closing in favor of #127, which carries your commits forward. The category axis stays open as #132 — that's the piece worth getting right, and your design input there would be welcome.

@timvw timvw closed this Aug 19, 2026
timvw added a commit that referenced this pull request Aug 19, 2026
…127)

Adds `wt clone <owner/repo|url> [dest]`, which acquires the main repository so
that `wt create`/`checkout` have something to hang worktrees off. Placement is
driven by two flat top-level settings:

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

The trailing branch segment is the remote's default branch, resolved with
`git ls-remote --symref` before the clone, so the clone directory is a valid
main-worktree slot rather than a bare repo folder.

`owner/repo` resolves through `gh` or `glab`, each asked for its own
`git_protocol`. Account selection stays with those tools (GH_CONFIG_DIR,
GH_TOKEN, GLAB_HOST); wt never mutates their global auth state.

Extra grouping levels compose through the pattern rather than being named by
wt: `{.env.WT_CATEGORY}` gives back the "category" layer without wt owning a
vocabulary. Durable, non-ephemeral grouping is tracked separately in #132.

Based on #122 by @brosu, whose commits are carried forward here.

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

timvw commented Aug 20, 2026

Copy link
Copy Markdown
Owner

@brosu — following up now that both halves of what this PR was reaching for are on main.

1. wt clone with category-based placement — landed in #127. repo_root (default ~/dev/repos) and repo_pattern control placement, and the trailing {.branch} segment makes the clone a normal main-worktree slot so sibling strategies place feature worktrees next to it:

wt clone timvw/wt                       # owner/repo resolved via gh/glab
wt clone git@github.com:me/dotfiles.git # full URL used as-is
wt clone acme/api ~/src/api             # explicit destination

2. Categories — the [categories.*] registry from this PR was deliberately not adopted, on the grounds that wt should not own the vocabulary (work / personal / oss). The capability is there, expressed with an environment variable in the pattern:

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

The :- default is #139, added precisely because the env-only answer hard-errored when the variable was not exported. And if you want the category set per directory rather than per command, #143 just documented a direnv recipe that needs no wt configuration at all. Native support for path-based rules is designed and tracked in #138, currently parked on demand rather than on open questions.

Two things worth being straight about:

  • The auth profile part of this PR (gh_auth, git_protocol, glab_host) was not adopted and is not planned. gh auth switch mutates gh's global hosts.yml and flips the active account for every other process on the machine; account selection belongs to GH_CONFIG_DIR / GH_TOKEN / GLAB_HOST, which wt clone respects.
  • None of this is in a tagged release yet — v0.1.33 predates it. You will need to build from main until the next release.

Thanks for the original push here; the shape of wt clone came out of this PR even though the implementation went a different route.

@timvw

timvw commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Correction to the last point above: #138 is no longer parked — it shipped in #145, so the category can now be set per directory without direnv:

# ~/.config/wt/config.toml
[[context]]
when_path = "~/dev/repos/work"
env = { WT_CATEGORY = "work" }
wt clone acme/api    # ~/dev/repos/work/acme/api/main, no export needed

Matching is a path prefix, all matching rules apply (later wins per variable), and an actual exported WT_CATEGORY still overrides a rule for a one-off. For repo-scoped commands like wt create the rule matches the repository's main checkout, not the working directory, so every linked worktree of that repo resolves to the same category even when worktree_root puts them in a different tree — which is the one thing the direnv recipe cannot do without an .envrc in both trees.

Rules are read from ~/.config/wt/config.toml only for now; reading them from global git config is the remaining follow-up on #138. Still main-only, not in a tagged release.

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