Help us shape the future of Go! Take the 2026 Go Developer Survey

wtg

command module
v1.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 7 Imported by: 0

README

wtg

wtg manages feature branches that span multiple repos. When a feature touches several repos at once, wtg checks out a shared branch across all of them as git worktrees and wires them together with a go.work file — giving you an isolated, ready-to-build workspace per feature without cloning anything new.

New to worktrees? The GitKraken worktree guide is a good primer.

Installation

For using this fork:

go install github.com/suhlig/wtg@latest
Shell completions

bash — add to ~/.bashrc:

source <(wtg completion bash)

zsh — add to ~/.zshrc:

source <(wtg completion zsh)

fish — add to ~/.config/fish/conf.d/wtg.fish:

if status is-interactive
  wtg completion fish | source
end

Configuration

Run wtg config init to scaffold a commented config file at ~/.config/wtg/config.toml:

wtg config init

It writes every setting commented out with its default; uncomment and edit the lines you want to override. wtg config prints the resolved file, wtg config edit opens it in $EDITOR, and wtg config path prints its path. A minimal config looks like:

[discovery]
root_dir = "~/repos"       # where wtg scans for git repos (or use root_dirs = ["~/repos", "~/work/repos"])
max_depth = 2

[spaces]
root_dir = "~/workspaces"  # where workspaces are created

[git]
branch_prefix = ""         # prepended to workspace names, e.g. "yourname/"

[archive]
root_dir = "~/repos-archived"  # where `wtg repo archive` moves retired clones

YAML is still accepted: a file ending in .yaml/.yml loads via its extension, so an existing config.yaml keeps working.

discovery.root_dir (or discovery.root_dirs for multiple search directories) should contain your regular repo clones, each sitting on their default branch (main, master, etc.) and otherwise left untouched. wtg creates worktrees alongside them — it never modifies the main clones.

Repos are addressed by their slash-separated path relative to their discovery root directory, e.g. github.com/suhlig/rustomato. A repo nested under org or group directories can also be addressed by its basename (rustomato) as long as no other discovered repo shares that basename; otherwise wtg errors and lists the matching repos, and you use the full path to disambiguate. A unique partial match on any path segment works too — infra selects github.com/suhlig/infrastructure, and suhlig selects a repo under that org — but when more than one repo matches, wtg lists all candidates and you disambiguate with a longer name or the full path.

Override with --config <path> or the WTG_CONFIG environment variable.

Always-included repos, files, and hooks

The optional always section applies the same setup to every new space:

[always]
repos = ["shared-tooling"]          # symlinked into every new space
files = ["~/.config/wtg/CLAUDE.md"] # copied into every new space root
secrets = [".env"]                  # copied into each worktree when present in the source repo
run = "~/.config/wtg/on-event"      # executable run after create/add/remove/delete

always.repos symlinks shared repos in (no feature-branch worktree), always.files seeds template files into the space root, always.secrets copies listed local files from each source repo into its worktree when present, and always.run invokes a hook script on space lifecycle events with the space context in WTG_* environment variables. See docs/always.md for the full behaviour, the event/variable reference, and examples.

Quick start

# Create a workspace for a new feature across three repos
wtg new my-feature api payments frontend

# Jump in
wcd my-feature

# ... do your work, then clean up
wtg delete my-feature --delete-branch

Workspace commands

wtg new <workspace> <repo>...

Create a workspace. At least one repo must be specified. For each repo, wtg creates or checks out a branch named <branch_prefix><workspace> as a linked worktree. A go.work file is written automatically for repos that have a go.mod. Paths listed in always.secrets are copied from each source repo into its worktree when present (see docs/always.md).

wtg new my-feature api payments frontend
wtg new my-feature api --branch yourname/main  # check out an existing branch

Branch behaviour per repo:

Branch state Action
Does not exist Created from the repo's default branch
Exists, not checked out Checked out in the new worktree
Exists, already checked out Error
wtg delete <workspace>

Delete a workspace and remove its worktrees. Prompts for confirmation if any repo has uncommitted changes or unpushed commits.

wtg delete my-feature            # remove worktrees, keep branches
wtg delete my-feature -d         # also delete branches if merged
wtg delete my-feature -D         # force-delete branches
wtg add [<workspace>] <repo>...

Add repos to an existing workspace. Creates worktrees on the workspace's branch and updates go.work. When run from inside a workspace directory, the workspace argument can be omitted.

wtg add my-feature infra logging
wtg add infra logging             # workspace inferred from CWD
wtg remove [<workspace>] <repo>...

Remove repos from a workspace. Prompts if there are uncommitted changes or unpushed commits. Use wtg delete to remove the whole workspace. When run from inside a workspace directory, the workspace argument can be omitted.

wtg remove my-feature logging
wtg remove my-feature logging -d  # also delete the branch
wtg remove logging                # workspace inferred from CWD
wtg push [<workspace>]

Push the workspace's branch from each repo's worktree to origin in parallel. When run from inside a workspace directory, the workspace argument can be omitted.

wtg push my-feature
wtg push                          # workspace inferred from CWD

If a push is rejected because the branch has diverged from its upstream (typically after wtg rebase), the output says so and shows the git push --force-with-lease command. wtg never force-pushes.

wtg rebase [<workspace>] [--onto <branch>] [--progress]

Fetch origin and rebase the workspace's branch onto origin/<default branch> in every repo's worktree, in parallel, with --autostash so uncommitted changes come along. The default branch is resolved per repo; --onto names one branch for all repos. When run from inside a workspace directory, the workspace argument can be omitted.

wtg rebase my-feature
wtg rebase                        # workspace inferred from CWD
wtg rebase --onto develop         # rebase onto origin/develop in every repo
wtg rebase --progress             # live per-repo progress bar
  • Repos that are already up to date are reported as ✓ up to date; rebased repos as ↑ rebased (was a1b2c3d). Undo with git reset --keep a1b2c3d.
  • A conflict aborts that repo's rebase and restores the worktree, including uncommitted changes; the command to retry by hand is printed.
  • If re-applying the stash conflicts after a successful rebase, the branch is rebased, conflict markers are in the worktree, and the changes stay in the stash.
  • Repos on another branch, with unresolved conflicts, or with a rebase, merge, cherry-pick or revert in progress are skipped.
  • The exit code is non-zero if any repo failed or needs attention. Nothing is pushed.

See ADR 0018 for the design.

wtg status [<workspace>...]

Show workspace status. Without arguments, shows all workspaces — the one containing the current directory is listed first. Pass --long / -l to expand file-level changes per repo.

wtg status
wtg status my-feature
wtg status my-feature --long
my-feature  ~/workspaces/my-feature
  api       [geoff/my-feature]  ✓ clean      ↑2
  payments  [geoff/my-feature]  ! 2 modified
  frontend  [geoff/my-feature]  ✓ clean
wtg exec [<workspace>] [--parallel] -- <cmd> [<args>...]

Run a command in each repo's worktree sequentially, streaming output as it goes. With --parallel, commands run in all repos concurrently with a live progress indicator, buffering per-repo output until complete.

Execution continues even if a command fails — all repos are attempted and failures are reported at the end. When run from inside a workspace directory, the workspace argument can be omitted.

wtg exec my-feature -- git status
wtg exec my-feature --parallel -- go test ./...
wtg exec -- git status            # workspace inferred from CWD

Repo commands

These operate on your main repo clones, not workspace worktrees. Useful for keeping clones up to date before starting a new feature.

wtg repo sync [<repo>...]

Fetch and fast-forward each repo's default branch. Repos with uncommitted changes are skipped with a warning.

wtg repo sync               # sync all repos
wtg repo sync api payments  # sync specific repos
wtg repo sync --progress    # watch per-repo progress live

See docs/repo-sync.md for the progress-bar symbol legend and state diagram.

wtg repo status [<repo>...]

Show branch, dirty status, and ahead/behind counts for each main repo clone.

wtg repo status
wtg repo status --long  # also show remote URL and local path
wtg repo archive <repo>...

Retire one or more main repo clones: each is moved out of the discovery area into archive.root_dir, kept intact, and recorded (with its original path) under ~/.local/share/wtg/archived.yaml. Nothing is deleted and, without --remote, no remote is touched. See docs/archive.md for the full behaviour, the provenance file, and examples.

wtg repo archive old-service spike-repo
wtg repo archive old-service --dry-run
wtg repo archive old-service --remote

Archiving refuses if a clone has uncommitted changes, commits not on any remote, or stashes; pass --force to proceed anyway. It also refuses, with no override, when a clone has live or stale worktrees, is still referenced by a space, or is listed in always.repos — the messages name the exact command that clears the block.

archive.root_dir must be on the same filesystem as your repos and outside every discovery root, or the moved clones would be rediscovered.

Pass --remote to also archive each repo on GitHub, as part of the same all-or-nothing operation. It requires the gh CLI (2.32 or newer) with an authenticated github.com account, and every named repo must have a github.com origin — if any does not, the whole command aborts rather than silently skipping it. It is idempotent: a repo that is already archived upstream is left as is. If an upstream archival fails, the local moves are rolled back and nothing is left archived.

Without --remote, when a repo's origin is on github.com, the exact gh command to archive it upstream is printed at the end instead:

Upstream repos were not modified. To archive them on GitHub:
  gh repo archive something/foo --yes
wtg repo unarchive <repo>...

Restore one or more archived repo clones: each is moved out of archive.root_dir back to the exact path recorded when it was archived, and its provenance record is removed. Nothing is deleted.

wtg repo unarchive old-service spike-repo
wtg repo unarchive old-service --dry-run
wtg repo unarchive old-service --remote

unarchive refuses if the recorded origin path already exists, or if the archived clone is missing from archive.root_dir; pass --archive-dir if you moved your archive root.

Pass --remote to also unarchive each repo on GitHub, under the same rules as --remote on archive: it requires the gh CLI (2.32 or newer) with an authenticated github.com account, every named repo must have a github.com origin, it is idempotent for a repo that is already unarchived upstream, and if an upstream unarchive fails the local restores are rolled back. Without --remote, the exact gh command is printed instead:

Upstream repos were not modified. To unarchive them on GitHub:
  gh repo unarchive something/foo --yes

Development

This fork renames the module path to github.com/suhlig/wtg so it installs via go install github.com/suhlig/wtg@latest. Upstream declares github.com/geoffamey/wtg, so merging upstream changes conflicts on the internal import lines. Enable git rerere once and Git replays the same resolution automatically on every future merge:

git config rerere.enabled true

Conflicts are limited to hunks that touch import blocks; changes elsewhere in a file merge cleanly.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
internal
archive
Package archive records where retired repo clones came from, so they can be found and restored later.
Package archive records where retired repo clones came from, so they can be found and restored later.
cmd
config
Package config handles loading and validating wtg configuration.
Package config handles loading and validating wtg configuration.
git
Package git provides the Runner interface and its system-git implementation.
Package git provides the Runner interface and its system-git implementation.
git/testhelper
Package testhelper creates real git repositories in t.TempDir() for use in integration tests of the git.Runner implementation.
Package testhelper creates real git repositories in t.TempDir() for use in integration tests of the git.Runner implementation.
remote
Package remote parses git remote URLs and derives host-specific actions from them.
Package remote parses git remote URLs and derives host-specific actions from them.
saga
Package saga provides a simple pre-flight + compensating-action transaction pattern.
Package saga provides a simple pre-flight + compensating-action transaction pattern.
state
Package state handles reading and writing space metadata to XDG data files.
Package state handles reading and writing space metadata to XDG data files.
ui
Package ui provides shared lipgloss styles and output formatting helpers.
Package ui provides shared lipgloss styles and output formatting helpers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL