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.

Agent skill

npx skills add errhythm/copse

Teaches coding agents to drive copse safely: always -r and --json, dry-run first, read exit codes, and remove branches only when copse created them.

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.2.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.2.0
File Size Uploaded
copse-0.2.0.tar.gz 16.2 kB Details

Built distribution (wheel)

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

Total release size: 28.3 kB

Release files / copse-0.2.0.tar.gz

Download URL copse-0.2.0.tar.gz
Size 16.2 kB
Tags Source
SHA-256 checksum
How to use checksums
5fc43aff370185b483530d1597de785167cd6fad2c3a30a06d2797307e5ffe17
BLAKE2b-256 checksum
How to use checksums
e6eec43884b913957bd9a42d1432f4fdb44cb10600b995dc01104ddb5379b22f
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.2.0-py3-none-any.whl

Download URL copse-0.2.0-py3-none-any.whl
Size 12.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
816a2c46bc9721927c8ef194569439755c65f50447d1d109ac8e8940b69973cf
BLAKE2b-256 checksum
How to use checksums
7891df004d2e175543c5ca2b100723630f695eb167366c6972815ca6a34bdef6
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

This release

0.2.0 This release

2 release files

0.1.0

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