Skip to content

Rewrite ta-wt in Python #21

@rjernst

Description

@rjernst

branch: rewrite-ta-wt-python

Spec: Rewrite ta-wt in Python

Overview

Rewrite scripts/ta-wt (818 lines of zsh) to Python 3 using stdlib only (no pip dependencies). The script is a git worktree manager with 6 subcommands (list, create, remove, prune, status, merge). The rewrite replaces hand-rolled arg parsing with argparse, manual JSON construction with the json module, and parallel arrays with dataclasses.

The file stays named ta-wt (no .py extension) with a #!/usr/bin/env python3 shebang so the ta dispatcher and setup symlink mechanism work unchanged.

Constraints:

  • stdlib only — argparse, json, subprocess, pathlib, dataclasses, shutil, os, sys
  • Python 3.9+ (ships with macOS, available on all CI runners)
  • Identical CLI interface — same subcommands, same flags, same exit codes
  • Identical JSON output schema — ta-workspace depends on it
  • Identical text output column widths

Architecture

scripts/ta-wt (Python, ~500-600 lines)
├── Git class          — subprocess wrapper (run, output methods)
├── Worktree           — dataclass (path, branch)
├── Core functions
│   ├── parse_worktrees()    — parse `git worktree list --porcelain`
│   ├── dirty_status()       — parse `git status --porcelain`
│   ├── ahead_behind()       — `git rev-list --left-right --count`
│   ├── classify_status()    — multi-stage classification
│   ├── has_active_operation() — detect rebase/merge/cherry-pick
│   └── default_wt_path()   — generate worktree path from branch
├── Subcommand handlers (cmd_list, cmd_create, cmd_remove, cmd_prune, cmd_status, cmd_merge)
├── build_parser()     — argparse with subparsers
└── main()

Git class

class Git:
    def __init__(self, cwd=None): ...
    def run(self, *args, check=True, cwd=None) -> CompletedProcess:
        """Run git command. cwd overrides instance default."""
    def output(self, *args, **kw) -> str:
        """Run and return stripped stdout."""

Subcommand CLI mapping

Subcommand Positional args Flags
list --json, --full
create branch, [path] --remote <name>, --from=<base>
remove branch --force
prune --apply
status --json
merge branch --target <branch>, --message <msg>, --message-file <path>

JSON output schemas (must be preserved exactly)

list --json:

[{"branch": "...", "status": "...", "ahead": 0, "behind": 0, "path": "..."}]

list --json --full:

[{"branch": "...", "status": "...", "ahead": 0, "behind": 0, "path": "...", "commit_message": "...", "commit_date": "..."}]

status --json:

[{"branch": "...", "status": "...", "ahead": 0, "behind": 0, "dirty": "...", "path": "..."}]

Note: ahead/behind must be integers (not strings). Field order must match the above.

Text output column widths

list: %-24s %-14s %-6s %-7s %s (BRANCH, STATUS, AHEAD, BEHIND, PATH)
list --full: %-24s %-14s %-6s %-7s %-30s %s (+ LAST COMMIT)
status: %-24s %-10s %s ahead, %s (BRANCH, classification, ahead, dirty)

Classification logic (classify_status)

Order matters — first match wins:

  1. current — worktree path matches CWD
  2. mergedgit merge-base --is-ancestor, or git merge-tree --write-tree tree comparison, or gh pr list --state merged (use shutil.which("gh"))
  3. wip — dirty working tree
  4. conflictgit merge-tree detects conflicts (old three-way form)
  5. almost — ahead of base with unpushed commits
  6. ready — ahead of base, all pushed (or no upstream)

Cross-script interaction

cmd_remove calls ta-workspace kill <branch> for cleanup. Find the script via os.path.dirname(os.path.realpath(__file__)) (equivalent of zsh ${0:A:h}). Suppress all errors.

Exit codes

  • 0: success
  • 1: runtime error (branch not found, dirty worktree, merge conflict)
  • 2: usage/argument error (argparse handles this automatically)

Error message prefix

All error messages use ta: prefix (e.g., ta: no worktree found for branch 'foo'), matching current behavior.


1. Core Functions

parse_worktrees(git) -> list[Worktree]

Parse git worktree list --porcelain. Skip bare worktrees. Handle detached HEAD as (detached). Return list of Worktree(path, branch) dataclass instances.

dirty_status(git, wt_dir) -> str

Parse git status --porcelain in the given directory. Count staged (S), modified (M), untracked (U). Return "clean" or "dirty(2S,1M,3U)" format.

ahead_behind(git, branch, base="main") -> tuple[int, int]

Run git rev-list --left-right --count {branch}...{base}. Return (ahead, behind). Return (0, 0) for detached or on error.

classify_status(git, wt_dir, branch, current_wt, base="main") -> str

Implement the multi-stage classification described above. For squash-merge detection:

  • git merge-tree --write-tree base branch — compare first line of output to git rev-parse "base^{tree}"
  • gh pr list --head branch --state merged --json number --limit 1 — check for non-empty result

has_active_operation(git, wt_dir) -> bool

Check for rebase-merge/, rebase-apply/ dirs and MERGE_HEAD, CHERRY_PICK_HEAD files in the git dir.

default_wt_path(git, branch) -> str

Get repo root via git rev-parse --show-toplevel. Sanitize branch (replace / with -). Return {parent}/{repo_name}-{sanitized_branch}.

2. Subcommands

cmd_list

Parse worktrees, compute dirty status and ahead/behind for each. Output as text table or JSON. With --full, include commit message (truncated to 25 chars + ... if > 28) and commit date (ISO 8601).

cmd_create

Two modes:

  • Tracking (default): validate branch exists on remote via git ls-remote, then git worktree add --track -b.
  • --from=<base>: create new local branch from base, or checkout existing branch if it's a descendant of base.

Remote resolution: --remote flag > upstream remote > origin remote.

cmd_remove

Find worktree by branch name (parse porcelain). Check dirty status unless --force. Remove worktree, delete local branch with git branch -D, call ta-workspace kill.

cmd_prune

Find merged worktrees (same three-stage detection as classify_status). Skip main, detached, current, dirty, and active-operation worktrees. Dry-run by default, --apply to execute.

cmd_status

Classify each non-main worktree. Output as text or JSON.

cmd_merge

Squash-merge branch into target (default: main). Validate both worktrees are clean. On conflict, abort and reset. Commit with --message, --message-file, or default message. Clean up worktree via cmd_remove --force.


Implementation Plan

Each step follows this structure:

  1. Implement — Write the code
  2. Test — Write BATS tests
  3. Verify — Run tests, fix failures until all pass
  4. Review — Code review for bugs, edge cases, and conventions
  5. Address feedback — Fix review findings, re-run tests, re-review until clean
  6. Update spec — Mark the step [done] and record any decisions or deviations

Spec maintenance rules

  • Mark each step [done] when complete.
  • Record design decisions that emerged during implementation as notes under the step.
  • Minor deviations (e.g. flag name changes, reordered logic) should be noted and the spec updated to match.
  • Significant design changes (e.g. new subcommands, changed architecture, removed features) require pausing for user review before proceeding.

Step 1: Write Python ta-wt with core infrastructure and list subcommand [done]

Files:

  • scripts/ta-wt — Replace zsh with Python

Implement:

  1. Add #!/usr/bin/env python3 shebang
  2. Implement Git class with run() and output() methods
  3. Implement Worktree dataclass
  4. Implement parse_worktrees(), dirty_status(), ahead_behind()
  5. Implement cmd_list with text and JSON output (both --json and --full)
  6. Implement build_parser() with list subparser
  7. Implement main() with dispatch
  8. chmod +x scripts/ta-wt

Test: Run list-related bats tests.

Verify: Run bats tests/test_ta_wt.bats — list tests pass.

Review: Verify JSON schema matches exactly (field names, types, order). Verify text column widths match.

Address feedback: Fix all review findings. Re-run tests. Re-review if changes were substantial.

Step 2: Add create subcommand [done]

Files:

  • scripts/ta-wt — Add create subcommand

Implement:

  1. Implement default_wt_path()
  2. Implement cmd_create with both tracking and --from modes
  3. Add create subparser to build_parser()

Test: Run create-related bats tests.

Verify: Run bats tests/test_ta_wt.bats — create tests pass.

Review: Verify remote resolution logic (upstream > origin fallback). Verify branch validation and error messages.

Address feedback: Fix all review findings. Re-run tests. Re-review if changes were substantial.

Step 3: Add remove subcommand [done]

Files:

  • scripts/ta-wt — Add remove subcommand

Implement:

  1. Implement cmd_remove with dirty check and --force flag
  2. Implement ta-workspace kill cleanup call using os.path.realpath(__file__)
  3. Add remove subparser

Test: Run remove-related bats tests.

Verify: Run bats tests/test_ta_wt.bats — remove tests pass.

Review: Verify workspace cleanup is best-effort (errors suppressed). Verify branch deletion uses -D flag.

Address feedback: Fix all review findings. Re-run tests. Re-review if changes were substantial.

Step 4: Add status and prune subcommands [done]

Files:

  • scripts/ta-wt — Add status and prune subcommands

Implement:

  1. Implement classify_status() with full multi-stage logic
  2. Implement has_active_operation()
  3. Implement cmd_status with text and JSON output
  4. Implement cmd_prune with dry-run/apply modes
  5. Add status and prune subparsers

Test: Run status and prune bats tests.

Verify: Run bats tests/test_ta_wt.bats — status and prune tests pass.

Review: Verify classification order matches zsh version exactly. Verify squash-merge detection (merge-tree + gh fallback). Verify prune skips main/detached/current/dirty/active-op worktrees.

Address feedback: Fix all review findings. Re-run tests. Re-review if changes were substantial.

Step 5: Add merge subcommand

Files:

  • scripts/ta-wt — Add merge subcommand

Implement:

  1. Implement cmd_merge with squash merge, conflict detection, and cleanup
  2. Support --target, --message, --message-file flags
  3. Validate mutual exclusivity of --message and --message-file
  4. Add merge subparser

Test: Run merge-related bats tests.

Verify: Run bats tests/test_ta_wt.bats — merge tests pass.

Review: Verify conflict detection aborts and resets cleanly. Verify --message-file validation. Verify worktree cleanup after successful merge.

Address feedback: Fix all review findings. Re-run tests. Re-review if changes were substantial.

Step 6: Update bats test invocations [done]

Files:

  • tests/test_ta_wt.bats — Update all test invocations

Implement:

  1. Replace all run zsh "$TA_WT" with run "$TA_WT" throughout the file
  2. Verify no other zsh-specific invocation patterns remain

Test: Run all bats tests.

Verify: Run bats tests/test_ta_wt.bats — all 104 tests pass.

Review: Verify no zsh references remain in test invocations.

Address feedback: Fix all review findings. Re-run tests. Re-review if changes were substantial.

Step 7: Run all checks

Implement:

  1. Run the full test suite: bats tests/test_ta_wt.bats
  2. Run python3 -m py_compile scripts/ta-wt to verify syntax
  3. Fix any failures

Verify: All checks pass clean.

Step 8: Create commit

Implement:

  1. Stage scripts/ta-wt and tests/test_ta_wt.bats
  2. Create a commit: Rewrite ta-wt in Python (stdlib only)

Verify: git log -1 shows the commit.


Conventions

  • Language: Python 3.9+ (stdlib only)
  • Tests: BATS (existing regression suite, updated invocations)
  • Error messages: Prefix with ta: (e.g., ta: no worktree found for branch 'foo')
  • Exit codes: 0=success, 1=runtime error, 2=usage error

Metadata

Metadata

Assignees

No one assigned

    Labels

    specRalph spec for automated execution

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions