Skip to main content

Git Worktree Wrapper - CLI tool for managing git worktrees with configurable path templates

Project description

English | Русский

⚠️ Warning: GWW is not heavily tested, use at your own risk!

🚀 GWW - Git Worktree Wrapper

CI

A CLI tool that wraps git worktree functionality with configurable path templates, condition-based routing, and project-specific actions.

✨ Features

  • 📝 Configurable path templates: Dynamic path generation using templates with functions like path(n), branch(), norm_branch(), tag()
  • 🔄 Condition-based routing: Route repositories to different locations based on URI conditions (host, path, protocol, tags)
  • 🏷️ Tag support: Pass custom tags via --tag option for conditional routing and path organization
  • ⚙️ Project actions: Execute custom actions (file copies, commands) after clone or worktree creation
  • 🐚 Shell completion: Bash, Zsh, and Fish completion support

📋 Requirements

  • 🐍 Python 3.11+
  • 🔧 Git
  • 🖥️ Unix-like system (Linux, macOS)

📦 Installation

Install from PyPI (recommended)

The package is published on PyPI as git-worktree-wrapper; the CLI command it installs is gww.

Using uv

uv tool install git-worktree-wrapper
gww --help

Using pipx

pipx install git-worktree-wrapper
gww --help

Using pip

python -m pip install --user git-worktree-wrapper
gww --help

From Git (no PyPI)

Use this if you need a specific commit or branch that hasn't been released yet.

Using uv

uv tool install "git+https://github.com/vadimvolk/git-worktree-wrapper.git"
gww --help

Using pipx

pipx install "git+https://github.com/vadimvolk/git-worktree-wrapper.git"
gww --help

From source (development)

# Clone the repository
git clone git@github.com:vadimvolk/git-worktree-wrapper.git
cd git-worktree-wrapper

# Install with uv
uv sync

# Run gww
uv run gww --help

From source using pip

# From a local checkout
cd git-worktree-wrapper
python -m pip install .
gww --help

🚀 Quick Start

1. ⚙️ Initialize Configuration

gww init config

This creates a default configuration file at ~/.config/gww/config.yml (Linux), ~/Library/Application Support/gww/config.yml (macOS), or %APPDATA%\gww\config.yml (Windows). On any platform, exporting XDG_CONFIG_HOME to an absolute path overrides the default and stores the config there instead. Edit these 2 values: default_sources and default_worktrees. Check the tutorial section for routing details.

2. 🐚 Initialize Shell Integration

gww init shell zsh  # or bash, or fish

This installs shell completion and aliases (gwc, gwa, gwr) for easier workflow. Follow the instructions printed by the command to enable them in your shell.

3. 📥 Clone a Repository

gwc https://github.com/user/repo.git
# Prompts: "Navigate to ~/Developer/sources/github/user/repo? [Y/n]"
# Navigates if you confirm (default: yes)

4. ➕ Add a Worktree

cd ~/Developer/sources/github/user/repo
gwa feature-branch
# Prompts: "Navigate to ~/Developer/worktrees/github/user/repo/feature-branch? [Y/n]"
# Navigates if you confirm (default: yes)

5. ➖ Remove a Worktree

gwr feature-branch
# If worktree has uncommitted changes or untracked files:
#   Prompts: "Force removal? [y/N]"
#   Removes with --force if you confirm
# Otherwise: Removes worktree immediately
# Output: Removed worktree: ~/Developer/worktrees/github/user/repo/feature-branch

6. 🔄 Update Source Repository

gww pull
# Output: Updated source repository: ~/Developer/sources/github/user/repo

Note: gww pull updates the source repository even from a worktree, as long as the source is clean and has main or master checked out. Useful for merge/rebase workflows.

gww pull # from any repository worktree
git rebase main # rebase your current changes to updated main branch

7. 🚚 Migrate Repositories

Create a backup first!

gww migrate ~/old-repos --dry-run
# Output:
# Would migrate 5 repositories:
#   ~/old-repos/repo1 -> ~/Developer/sources/github/user/repo1
#   ...

gww migrate ~/old-repos
# Copy (default): list, copy sources then worktrees, repair, summary

gww migrate ~/old-repos --inplace
# Move worktrees then sources, repair, clean empty folders

The migrate command scans one or more directories for git repositories and migrates them to locations based on your current configuration. It's useful when:

  • You've updated your configuration and want to reorganize existing repositories
  • You're moving from manual repository management to GWW
  • You need to consolidate repositories from different locations

Options:

  • --dry-run, -n: Show what would be migrated without making changes
  • --copy (default): Copy repositories to new locations; list, validate, copy sources then worktrees, run git worktree repair, then report summary. No folder cleanup.
  • --inplace: Move repositories in place (worktrees first, then sources), run git worktree repair, then recursively clean empty source folders.

Behavior:

  • Accepts one or more paths; scans each and merges repo lists (deduplicated)
  • Classifies each repo as source or worktree; uses source path template for sources and worktree path template for worktrees
  • --inplace: Two passes (worktrees then sources), move and repair, then remove vacated dirs and empty parents up to input roots
  • --copy: List sources and worktrees, validate destinations, copy sources then worktrees, repair relations, report summary
  • Skips repositories without remotes, detached HEAD worktrees, or already at target

Tutorial

A minimal config file looks like:

# Folder where all sources are checked out with gwc. path(-2)/path(-1) generates 2-level subfolders based on repository URI. Like https://github.com/user/repo.git -> ~/Developer/sources/user/repo
default_sources: ~/Developer/other/sources/path(-2)/path(-1)
# Folder where all worktrees are checked out with gwa. norm_branch() works better with remote branches, e.g. origin/remote-branch -> origin-remote-branch
default_worktrees: ~/Developer/other/worktrees/path(-2)/path(-1)/norm_branch()

The generated file will have more options commented out, including the functions reference.

Checkout based on where repository is hosted

Useful to separate e.g. open source projects (where you learn or get inspired) from your work projects.

# Still needed in case the config fails to find a section. You may prefer a non-nested sources structure, but make sure the result folder is unique
default_sources: ~/Developer/sources/host()-path(-2)-path(-1)
default_worktrees: ~/Developer/worktrees/host()-path(-2)-path(-1)-norm_branch()
sources:
  # ... other rules
  work:
    when: "your.org.host" in host()
    sources: ~/Developer/work/sources/path(-2)-path(-1)
    worktrees: ~/Developer/work/sources/path(-2)-path(-1)-norm_branch()
  

That's enough to separate work sources from all others, but you can create more sections with various rules. The library uses simpleeval to evaluate templates, so you can use its operators and functions below to get necessary routing.

🌐 URI Functions (available in templates and when conditions)

Function Description Example
uri() Get full URI string uri()"https://loca-repo-manager.com:8081/user/repo.git"
host() Get URI hostname host()"loca-repo-manager.com"
port() Get URI port (empty string if not specified) port()"8081" or "" usually
protocol() Get URI protocol/scheme protocol()"https" / "ssh" / git
path(n) Get URI path segment by index (0-based, negative for reverse) path(-1)"repo", path(0)"user"

🌿 Branch Functions (available in templates)

Function Description Example
branch() Get current branch name branch()"feature/new/ui"
norm_branch(replacement) Branch name with / replaced (default: "-") norm_branch()"feature-new-ui", norm_branch("_")"feature_new_ui"

Need to checkout temporary projects separately? Add this to your config:

sources:
  # ... other rules
  temp:
    when: tag_exist("temp")  # See [tags section](#-tags) for details about tags
    sources: ~/Downloads/temp/sources/time_id()-host()-path(-2)-path(-1) 
    worktrees: ~/Downloads/temp/worktrees/time_id()-host()-path(-2)-path(-1)-norm-branch()

time_id(fmt) generates a datetime-based identifier (cached per template evaluation). Default format is "20260120-2134.03" (short, seconds accuracy unique). Use format codes for more detailed/nested results. Works properly if used multiple times.

worktrees: ~/Downloads/temp/worktrees/time_id("%Y")/time_id("%m")/time_id("%H-%M$.%S")/host()-path(-2)-path(-1)-norm-branch()

Generates nested structure: YYYY/HH-MM.ss/host()-path(-2)-path(-1)-norm-branch()

⚙️ Actions (available in actions section)

Run actions after checking out a repository, after adding a worktree, or before removing a worktree. Common example: copying local.properties for Gradle projects.

actions:
  - when: file_exists("settings.gradle") # Check if it's actually a Gradle project
    after_clone:
      - copy: ["~/sources/default-local.properties", "local.properties"] # Copies your default file right after cloning the repo
    after_add: 
      - copy: ["source_path('local.properties')", "local.properties"] # Inherit existing repository file to worktree

You can have multiple when subsections in actions. After clone/add, the library goes top-to-bottom and executes all actions with matching when conditions. Other functions available in the actions section:

Action Description Example
copy Copy a file or directory tree from a template-evaluated source to a template-evaluated destination (relative to current_worktree()) copy: ["source_path('local.properties')", "local.properties"] or copy: ["~/sources/default-local.properties", "local.properties"]
command Execute external command (runs in destination directory, template functions available) command: "npm install" or command: "claude init"

Each action rule also accepts an optional critical: flag (default true). When true, a failing action in that rule aborts the remaining actions in the rule and makes the command exit 1. Set critical: false for non-critical rules — failures are reported in the action execution summary but the command still exits 0. See Failure handling below for the full exit-code table.

actions:
  - when: file_exists("package.json")
    critical: false  # non-critical: a missing node_modules is annoying, not fatal
    after_clone:
      - command: "npm install"
🪓 before_remove — cleanup actions before gww remove

A third action kind, before_remove, runs user-defined cleanup steps before git worktree remove deletes the worktree. Use it to archive the worktree, post a notification, run a hook, etc. The same critical: / command: / copy: machinery applies; a critical before_remove failure aborts the remove and exits 1, a non-critical failure is reported but the remove still proceeds. --force is git-only and does not bypass before_remove.

In before_remove rules, current_worktree() is the worktree being removed, source_path() is its parent source repo, and branch() resolves to the worktree's currently checked-out branch (or "" on detached HEAD). The gww remove command also accepts --tag key=value, which flows into tag() / tag_exist() predicates so per-call cleanup can be tag-driven.

actions:
  # Critical: archive the worktree before letting `gww remove` delete it.
  - when: 'tag_exist("archive")'
    before_remove:
      - command: "tar -czf ~/archives/norm_branch()-time_id('%Y%m%d').tar.gz current_worktree()"

  # Non-critical: notify a Slack channel. A failed notification should not block the remove.
  - when: tag("notify") == "slack"
    critical: false
    before_remove:
      - command: "curl -sf -X POST https://hooks.slack.com/... -d branch=branch()"

  # Path-based remove: when removing by absolute path, `branch()` still resolves.
  - when: branch() == "main"
    before_remove:
      - command: "echo refusing to remove main branch"

Note: gww remove accepts an absolute worktree path as well as a branch name. When invoked with a path, branch() reads the worktree's current branch via git rev-parse --abbrev-ref HEAD and falls back to "" for detached HEAD, so predicates referencing branch() never raise.

❗ Failure handling

Action failures are reported per-rule, grouped at the end of the action loop on stderr as the action execution summary. The summary lists every failing rule by its index in actions:, its criticality flag, and the failing action's error. Its non-emptiness also gates the success line: say() (the line scripts cd $(gwc …)) is suppressed whenever the summary has any entry, so cd only ever lands on a fully-configured worktree.

Outcome Exit code
Clean run (no failures) 0
Non-critical rule failure only 0
Critical rule failure 1
when: predicate or command: template failed to evaluate 2

📁 Actions Functions (available in command actions and when conditions)

Function Description Example
source_path(extra?) Get absolute path to source repository, optionally joined with extra source_path()"/path/to/repo", source_path("local.properties")"/path/to/repo/local.properties"
current_worktree(extra?) Get absolute path to the current worktree, optionally joined with extra current_worktree()"/path/to/worktree", current_worktree("local.properties")"/path/to/worktree/local.properties"
file_exists(path) Check if file exists relative to source repository file_exists("local.properties")True
dir_exists(path) Check if directory exists relative to source repository dir_exists("config")True
path_exists(path) Check if path exists (file or directory) relative to source repository path_exists("local.properties")True

🏷️ Tags

Still not flexible enough? Here comes tags. Tags specified using command line param -t <tag-name>[=optional value] (or --tag) for clone / add commands. Tags available in configuration with:

Function Description Example
tag(name) Get tag value by name (returns empty string if not set) tag("env")"prod"
tag_exist(name) Check if tag exists (returns boolean) tag_exist("env")True

🏷️ Tag Usage Example:

sources:
  # Temporary checkout: Clone repositories to ~/Downloads/temp for quick access
  # Usage: gwc <uri> -t temp
  temp:
    when: 'tag_exist("temp")'
    sources: ~/Downloads/temp/time_id()-host()-path(-1)
    worktrees: ~/Downloads/temp/time_id()-host()-path(-1)/norm_branch()

  # Code review worktrees: Add worktrees to ~/Developer/worktree/code-review for review tasks
  # Usage: gwa <branch> --tag review
  review:
    when: 'tag_exist("review")'
    worktrees: ~/Developer/review/worktree/path(-1)/norm_branch()
    # If used during clone, default source path is used

```bash
# Clone to temporary location
gwc https://github.com/user/repo.git -t temp
# Output: ~/Downloads/temp/repo

# Add worktree for code review
cd ~/Developer/sources/github/user/repo
gwa feature-branch --tag review
# Output: ~/Developer/worktree/code-review/repo/feature-branch

📖 Commands

Command Description
gwc <uri> [--tag key=value]... 📥 Clone repository to configured location (tags available in templates/conditions)
gwa <branch> [-c] [--tag key=value]... ➕ Add worktree for branch (optionally create branch, tags available in templates/conditions)
gwr <branch|path> [-f] [--tag key=value]... ➖ Remove worktree (tags available in before_remove predicates)
gww pull 🔄 Update source repository (works from worktrees if source is clean and on main/master)
gww migrate <path>... [--dry-run] [--copy | --inplace] 🚚 Migrate repositories to new locations
gww init config ⚙️ Create default configuration file
gww init shell <shell> 🐚 Install shell completion (bash/zsh/fish)

Note: gwc, gwa, and gwr are convenient shell aliases for gww clone, gww add, and gww remove respectively. They provide the same functionality with automatic navigation prompts. Install them with gww init shell <shell>.

Common Options:

  • --tag, -t: Tag in the format key=value or just key (can be specified multiple times).

🔄 Update

Using uv

# Re-run the install command to update to the latest version
uv tool install "git+https://github.com/vadimvolk/git-worktree-wrapper.git"

# Or use the update command (if available)
uv tool update gww

Using pipx

pipx upgrade gww

Using pip

python -m pip install --upgrade gww

🗑️ Uninstall

Using uv

uv tool uninstall gww

Using pipx

pipx uninstall gww

Using pip

python -m pip uninstall gww

🛠️ Development

🧪 Running Tests

# Run all tests
uv run pytest

# Run with coverage
uv run pytest --cov

# Run only unit tests
uv run pytest tests/unit/

# Run only integration tests
uv run pytest tests/integration/

🔍 Type Checking

uv run mypy src/gww

📄 License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

git_worktree_wrapper-0.1.0.tar.gz (181.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

git_worktree_wrapper-0.1.0-py3-none-any.whl (65.3 kB view details)

Uploaded Python 3

File details

Details for the file git_worktree_wrapper-0.1.0.tar.gz.

File metadata

  • Download URL: git_worktree_wrapper-0.1.0.tar.gz
  • Upload date:
  • Size: 181.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for git_worktree_wrapper-0.1.0.tar.gz
Algorithm Hash digest
SHA256 9fabd726d9e2d7f15580513efea6091b550264ecc0b82cbe3cdb467ad5c7f3a5
MD5 dd26854b9c505c3b68ab1464ec5ea024
BLAKE2b-256 9a30ed8e16aa20a3dd84916a80b29fd691afdb5aedb96db1e3295ea7db9191cc

See more details on using hashes here.

Provenance

The following attestation bundles were made for git_worktree_wrapper-0.1.0.tar.gz:

Publisher: release.yml on vadimvolk/git-worktree-wrapper

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file git_worktree_wrapper-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for git_worktree_wrapper-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 193ae5b6e3a0efd54bf7ae4c1eef8f41ba4087604ef877f742c1c66dabb7874d
MD5 8bcdd93505cd6d6e75d36bec7838aff6
BLAKE2b-256 eb2360a72b3e11b31311fe7bc7d892d57c3bdd64915abba81dd533860f56be78

See more details on using hashes here.

Provenance

The following attestation bundles were made for git_worktree_wrapper-0.1.0-py3-none-any.whl:

Publisher: release.yml on vadimvolk/git-worktree-wrapper

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page