Metadata-Version: 2.4
Name: cgwt
Version: 1.0.0
Summary: CLI tool to manage git worktrees, with a behavior that is modifiable with a configuration file.
License-Expression: MIT
License-File: LICENSE
Classifier: Environment :: Console
Classifier: Topic :: Software Development :: Version Control :: Git
Requires-Python: >=3.12
Project-URL: Repository, https://github.com/sylmarien/cgwt
Description-Content-Type: text/markdown

# cgwt

CLI tool to manage git worktrees, with a behavior that is modifiable with a configuration file.

## Install

```sh
uv tool install cgwt
```

`pipx install cgwt` works too. cgwt requires Python 3.12 or later. cgwt requires git 2.38 or later, tested against 2.43.

## Usage

cgwt creates each worktree in a workforest, at the path that the path pattern computes. A worktree is a linked git worktree inside a workforest. A workforest is a directory that holds the worktrees that cgwt creates. The path pattern is the configuration entry that computes the path of a worktree.

Every command requires the repository to have an `origin` remote. A repository is a directory that `git clone` or `git init` created, together with the worktrees that git records for it. A project is what the URL of `origin` identifies. Several repositories can belong to one project.

A command prints its result on stdout and its messages on stderr.

### create

```
cgwt create <branch> [--base <ref>] [--no-fetch] [--repo <path>] [--dry-run] [--json]
```

- create runs from any directory inside a repository.
- create runs `git fetch origin`. It then picks the branch from the first of these cases that applies:
  1. A local branch of that name exists. create uses it.
  2. `origin/<branch>` exists. create creates a local branch that tracks it.
  3. Neither exists. create creates a new branch from the base. The base is the commit that a new branch starts from.
- create prints the absolute path of the worktree on stdout.
- create refuses a branch that is checked out in the repository or in another worktree, with exit code 5.
- create refuses a path collision, with exit code 6. A path collision is a computed worktree path that already exists on disk.

| Flag | Effect |
| --- | --- |
| `--base <ref>` | Sets the base of a new branch for this invocation. create refuses the flag when the branch exists locally or on `origin`. |
| `--no-fetch` | Skips `git fetch origin`. |
| `--repo <path>` | Runs the command as if the current directory were `<path>`. |
| `--dry-run` | Prints the path on stdout and the source of each configuration key on stderr. It creates nothing and skips the fetch. |

```console
$ cgwt create feature-x
cgwt: created feature-x from origin/HEAD at /home/user/worktrees/meta.project/feature-x
/home/user/worktrees/meta.project/feature-x
```

### list

```
cgwt list [--project <glob>] [--branch <glob>] [--broken] [--json]
```

- list runs from any directory.
- list prints every worktree of every workforest: the top-level workforest and the workforest of every project override. A project override is a configuration entry that replaces the default settings for every project that it matches.
- list prints one line per worktree, sorted by path, with no header. The line is `<path>`, `<branch>`, and `<flags>`, separated by tabs. `<flags>` is empty or a comma-separated subset of `locked,broken`. A broken worktree is a worktree whose repository no longer exists.
- A glob matches the whole text and is case-sensitive. `*` is the only wildcard. It matches any run of characters, `/` included.

| Flag | Effect |
| --- | --- |
| `--project <glob>` | Keeps the worktrees whose project identifier matches the glob. |
| `--branch <glob>` | Keeps the worktrees whose branch name matches the glob. |
| `--broken` | Keeps only the broken worktrees. |

The output line of the example ends with a tab, because the worktree has no flag.

```console
$ cgwt list
/home/user/worktrees/meta.project/feature-x	feature-x	
```

### delete

```
cgwt delete <branch-or-path> [--force] [--no-fetch] [--repo <path>] [--json]
```

- The argument is the path of a worktree or a branch name. Inside a repository, a branch name selects among the worktrees of the project of that repository. Outside a repository, it selects among the worktrees of every workforest.
- The argument must match one worktree. Zero matches and several matches are errors with exit code 1.
- delete runs `git fetch --prune origin`. It then removes the worktree when it is a safe worktree. A safe worktree has no changed or untracked file, no git operation in progress, no lock, and no commit that both exists on no remote and is unmerged into the default branch of `origin`.
- delete refuses a worktree that is not safe. It prints one line per reason on stderr. The exit code is 4 when the worktree is locked, else 3.
- delete deletes the branch when it is merged into the default branch of `origin`. Otherwise it keeps the branch.
- delete removes the parent directories that the removal leaves empty, up to the workforest.
- delete prints the removed path on stdout.
- delete removes a broken worktree only with `--force`.

| Flag | Effect |
| --- | --- |
| `--force` | Skips every check except the lock. Deletes the branch whatever its merge state. |
| `--no-fetch` | Skips `git fetch --prune origin`. |
| `--repo <path>` | Runs the command as if the current directory were `<path>`. |

```console
$ cgwt delete feature-x
cgwt: deleted branch feature-x
/home/user/worktrees/meta.project/feature-x
```

### JSON output

With `--json`, a command prints JSON on stdout in place of the text.

| Command | JSON output |
| --- | --- |
| `list` | An array of worktree objects |
| `create`, `delete` | One worktree object |

A worktree object holds the keys `path`, `branch`, `project`, `repository`, `locked`, and `broken`.

## Configuration

cgwt reads `$XDG_CONFIG_HOME/cgwt/config.toml`. When `XDG_CONFIG_HOME` is unset, empty, or a relative path, cgwt reads `~/.config/cgwt/config.toml`. Without the file, cgwt uses the defaults. cgwt never writes the file. An unknown key is a configuration error with exit code 1.

Each of the four keys below is optional.

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `workforest` | string | `~/worktrees` | The directory that holds the worktrees. `~` expands to the home directory. |
| `path` | string | `{project}/{branch}` | The path pattern, relative to the workforest. |
| `fetch` | boolean | `true` | create and delete run `git fetch` first when the value is `true`. |
| `base` | string | `origin/HEAD` | The base of a new branch. The value can be any commit-ish. |

The `path` key holds the path pattern.

- A `{name}` placeholder names an input or a value.
- A list renders as several path segments.
- The path pattern must use `branch`, directly or through a value.
- The `{host}` placeholder requires a remote URL that has a host.

The Example column of the inputs table uses a repository in the directory `src`. The `origin` URL of that repository is `git@gitlab.example.com:platform/tools/meta.project.git`. The branch is `feature-x`.

| Input | Value | Example |
| --- | --- | --- |
| `host` | Host of the `origin` URL | `gitlab.example.com` |
| `remote_path` | Path segments of the `origin` URL, as a list | `platform/tools/meta.project` |
| `project` | Project name, the last path segment | `meta.project` |
| `main_worktree` | Directory name of the repository | `src` |
| `branch` | Branch name, with `/` replaced by `-` | `feature-x` |

```toml
# Flat per project (the default): ~/worktrees/meta.project/feature-x
path = "{project}/{branch}"
```

```toml
# Remote-mirroring: ~/worktrees/gitlab.example.com/platform/tools/meta.project/feature-x
path = "{host}/{remote_path}/{branch}"
```

```toml
# Split project name: ~/worktrees/meta.project/feature-x/meta/project/src
path = "{project}/{branch}/{project_parts}/{main_worktree}"

[values.project_parts]
input = "project"
split = "."
```

A `[values.<name>]` table computes a value.

| Key | Meaning |
| --- | --- |
| `input` | The key is required. It names an input or another value. |
| `split` | The separator that splits a string into a list. |
| `at` | The integer index of one element of a list. A negative index counts from the end. |
| `slash` | Valid only for a value that derives from `branch`. Sets the replacement for `/`. |

This example places the branch `feature/x`.

```toml
# Nested branch directories: ~/worktrees/meta.project/feature/x
path = "{project}/{branch_directories}"

[values.branch_directories]
input = "branch"
slash = "/"
```

A `[projects."<glob>"]` table sets `workforest`, `path`, `fetch`, or `base` for every project whose identifier the glob matches.

- The project identifier is `<host>/<path>` of the `origin` URL, without the scheme, the user, the port, and a trailing `.git`.
- The glob follows the glob rule in the description of `list` under Usage.

cgwt resolves each key on its own, from the first of these sources that sets the key:

1. A command-line flag.
2. The most specific matching project override that sets the key.
3. The top-level key.
4. The default.

```toml
workforest = "~/worktrees"

# Every project of gitlab.example.com: ~/work/worktrees/meta.project/feature-x
[projects."gitlab.example.com/*"]
workforest = "~/work/worktrees"
```

## Exit codes

| Code | Meaning |
| --- | --- |
| 0 | Success |
| 1 | Failure: configuration error, missing `origin`, missing `origin/HEAD`, ambiguous delete argument, failed git command, unexpected exception |
| 2 | Usage error |
| 3 | delete refused: the worktree is not safe, and `delete --force` would remove it |
| 4 | delete refused: the worktree is locked |
| 5 | create refused: the branch is checked out in the repository or in another worktree |
| 6 | create refused: path collision |

## Development

[uv](https://docs.astral.sh/uv/) manages the environment.

```sh
uv sync
uv run ruff check
uv run ruff format --check
uv run mypy
uv run pytest
```

pytest fails when line or branch coverage is below 100%.

## Release

The version lives in `pyproject.toml`. To release, run the Release workflow from the Actions tab and choose the version part to bump. The workflow bumps the version, tags the commit, publishes to PyPI, and creates a GitHub release.
