Skip to main content

copse

CI PyPI Python versions License

One folder per task, one git worktree per repo. Run it from any directory with repos 1-2 levels deep.

$ copse new 301
◇ Repos api, libs/auth-service-with-long-name
◇ Branch 301
◇ Start new branches from remote
╭─ Plan ───────────────────────────────────────────────╮
│ api                               301  ← origin/main │
│ libs/auth-service-with-long-name  301  ← main        │
╰──────────────────────────────────────────────────────╯
◆ Create 2 worktrees? Yes
✔ api                               301  tasks/301/api
✔ libs/auth-service-with-long-name  301  tasks/301/auth-service-with-long-name

What it does

  • Finds repos on its own. It scans 1-2 levels below the current directory for folders with a .git directory, skipping dot-folders, node_modules and existing worktrees.
  • Sets branch and base per repo with -r repo[:branch[:base]]. Branch defaults to the task name.
  • Starts new branches from origin/<base> (the default) or from your local <base> with --from local, and fetches first unless you pass --no-fetch.
  • Reuses a local branch if it exists, and tracks a remote-only branch instead of cutting a new one.
  • Shows every worktree's branch, dirty state and ahead/behind counts with copse ls.
  • Removes worktrees with copse rm, and deletes a branch only if copse created it for that task.
  • Speaks JSON and exit codes for scripts. Without -r and without a TTY it exits 2 instead of prompting, and git never asks for credentials.

Quick start

uv tool install copse          # or, from a clone: uv tool install -e .

cd ~/code                      # a folder that contains your repos
copse repos                    # see what it found
copse new 200 -r api -r web    # tasks/200/api and tasks/200/web
copse ls 200                   # branch, clean or dirty, ahead/behind
copse rm 200 --delete-branch   # clean up when the task is done

Run copse new 200 on a terminal with no -r to pick repos interactively.

Layout

~/code/
├── api/                # repos you already have
├── web/
├── libs/
│   └── auth-service/
└── tasks/
    └── 200/
        ├── api/        # worktree on branch 200
        └── web/

Worktrees go in ./tasks/<task>/. Change that with --root or the COPSE_ROOT environment variable. A nested repo is named by its last path part (auth-service), or libs-auth-service if another repo has the same folder name.

Commands

Command What it does
copse repos List detected repos with their current branch.
copse new TASK Create one worktree per repo under the task folder.
copse ls [TASK] Show worktrees per task: branch, dirty, ahead/behind.
copse rm TASK Remove a task's worktrees, all or only the -r repos.

copse new options:

Flag Default Meaning
-r, --repo SPEC interactive pick Repo to include. Repeat for more.
-b, --branch task name Branch for every repo without its own.
--base <remote>/HEAD, else current branch Base for new branches.
--from remote|local remote Cut new branches from <remote>/<base> or from local <base>.
--remote origin Remote to fetch and track.
--no-fetch off Skip git fetch.
--dry-run off Plan only, create nothing.
--root ./tasks ($COPSE_ROOT) Where task folders live.
--json off Machine-readable output.

A SPEC is repo[:branch[:base]]. repo is the id from copse repos or just the folder name if that is unique.

copse new 200 -r api                       # branch 200, default base
copse new 200 -r api:feat/200:develop      # own branch and base
copse new 200 --from local -r web:x:origin/main   # local start, but this repo uses origin/main

How branches are picked

For each repo, the first rule that matches wins.

  1. A local branch with that name exists. It is reused.
  2. <remote>/<branch> exists. A local branch is created that tracks it.
  3. Otherwise a new branch is cut from the base. A base written as origin/x always uses that remote ref, even with --from local. Otherwise --from remote uses origin/<base> when it exists and --from local uses <base>. A sha or tag also works.

With no base given, copse uses what <remote>/HEAD points at, or the repo's current branch if there is none. Rerunning new marks worktrees that already exist as exists.

For agents and scripts

Add --json to repos, new, ls or rm. new and rm print:

{
  "task": "200",
  "path": "/home/me/code/tasks/200",
  "results": [
    {"repo": "api", "path": ".../tasks/200/api", "branch": "200", "base": "main",
     "status": "created", "error": null}
  ]
}

status is created, exists, planned (dry run), removed or failed. Dry runs also include start, the ref the new branch would be cut from.

Code Meaning
0 Everything worked.
1 At least one repo failed. The others still ran.
2 Usage error, no -r without a TTY, or a cancelled prompt. Nothing was created.

Output is colored only on a TTY. Pipes, --json and NO_COLOR get plain text, and errors go to stderr as error: ....

copse new 200 -r api -r web:feat/200:develop --json | jq '.results[] | {repo,status,error}'

Removing tasks safely

copse rm 200 --delete-branch removes the worktrees, then deletes each branch only if copse created it for that task. Branches copse created carry a branch.<name>.copseTask git config entry. Branches you attached are kept and reported as branch_kept in JSON. Deletion uses git branch -d, so a branch with unmerged commits survives unless you add --force, which also forces worktree removal and uses -D.

Why copse

It was called twt for about a day, and I didn't like it. A copse is a small group of trees, which is what a task is here: a handful of worktrees standing together in one folder, one per repo.

Development

uv sync
uv run pytest

License

MIT, see LICENSE. NOTICE credits the projects some code was adapted from.

Metadata

Release files for copse 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for copse 0.1.0
File Size Uploaded
copse-0.1.0.tar.gz 15.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for copse 0.1.0
File Interpreter ABI Platform
copse-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 27.2 kB

Release files / copse-0.1.0.tar.gz

Download URL copse-0.1.0.tar.gz
Size 15.6 kB
Tags Source
SHA-256 checksum
How to use checksums
cf3db3735b17823d7af3f1eb766f2cf438875a390f4627f5eb775a0c83f6161f
BLAKE2b-256 checksum
How to use checksums
a80370201474239567849f325597a4870f6b77de3a6eeec447f12144509ed2e6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release files / copse-0.1.0-py3-none-any.whl

Download URL copse-0.1.0-py3-none-any.whl
Size 11.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
24edc9ac415f158cc853d8771495c9927c596a270c7666361f119ad990aa2e89
BLAKE2b-256 checksum
How to use checksums
5bf8b5d23d6853d3a044157c43e0127d4938ac52187e91195e4a5bdf52a6a7c5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page