gitstow 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gitstow-0.4.0 → gitstow-0.5.0}/AGENTS.md +48 -43
- {gitstow-0.4.0 → gitstow-0.5.0}/CHANGELOG.md +7 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/CLAUDE.md +4 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/PKG-INFO +1 -1
- gitstow-0.5.0/docs/building/plans/2026-07-16-move-repo-between-workspaces.md +95 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/pyproject.toml +1 -1
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/__init__.py +1 -1
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/manage.py +31 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/core/git.py +11 -0
- gitstow-0.5.0/src/gitstow/core/operations.py +349 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/routes/pages.py +9 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/routes/repos.py +22 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/templates/_repo_drawer.html +22 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_cli.py +53 -2
- gitstow-0.5.0/tests/test_move.py +600 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_serve.py +61 -0
- gitstow-0.4.0/src/gitstow/core/operations.py +0 -98
- {gitstow-0.4.0 → gitstow-0.5.0}/.claude/settings.json +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/.github/workflows/ci.yml +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/.github/workflows/publish.yml +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/.gitignore +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/BACKLOG.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/CODE_OF_CONDUCT.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/CONTRIBUTING.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/LICENSE +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/README.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/SECURITY.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/demo.gif +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/demo.tape +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/building/audit-2026-07-06.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/building/implementation-plan.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/building/plans/2026-07-06-wave-1-correctness-safety.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/building/plans/2026-07-06-wave-2-status-model.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/building/plans/2026-07-06-wave-3-structure-polish.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/building/plans/2026-07-12-ui-wave-a-make-it-real.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/building/plans/2026-07-12-ui-wave-b-polish.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/building/ui-audit-2026-07-12.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/user/commands.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/user/concepts.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/user/configuration.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/docs/user/getting-started.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/scripts/release.sh +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/__main__.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/__init__.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/add.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/config_cmd.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/doctor.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/exec_cmd.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/export_cmd.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/fetch.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/helpers.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/list_cmd.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/main.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/migrate.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/onboard.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/open_cmd.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/pull.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/remove.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/search.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/serve.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/setup_ai.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/shell.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/skill_cmd.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/stats.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/status.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/update.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/cli/workspace_cmd.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/core/__init__.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/core/collection_io.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/core/config.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/core/discovery.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/core/locking.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/core/parallel.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/core/paths.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/core/repo.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/core/status_model.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/core/url_parser.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/mcp/__init__.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/mcp/server.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/skill/SKILL.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/__init__.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/routes/__init__.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/routes/collection.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/routes/dashboard.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/routes/system.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/routes/workspaces.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/server.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/static/app.css +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/static/dashboard.js +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/static/fonts/bricolage-grotesque-latin-ext-var.woff2 +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/static/fonts/bricolage-grotesque-latin-var.woff2 +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/static/fonts/fonts.css +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/static/fonts/jetbrains-mono-latin-ext-var.woff2 +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/static/fonts/jetbrains-mono-latin-var.woff2 +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/static/vendor/VENDORED.md +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/static/vendor/htmx.min.js +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/templates/add_repo.html +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/templates/base.html +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/templates/dashboard.html +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/templates/partials/dashboard_rows.html +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/templates/partials/fetch_summary.html +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/templates/partials/pull_summary.html +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/templates/partials/repo_row.html +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/templates/settings.html +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/src/gitstow/web/templates/workspaces.html +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/__init__.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/conftest.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_collection_io.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_config.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_git.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_locking.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_mcp.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_onboard.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_operations.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_repo.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_setup_ai.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_status_model.py +0 -0
- {gitstow-0.4.0 → gitstow-0.5.0}/tests/test_url_parser.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# gitstow
|
|
1
|
+
# gitstow — AI Developer Guide
|
|
2
2
|
|
|
3
3
|
## What is this?
|
|
4
4
|
|
|
5
|
-
A CLI tool that manages collections of git repositories across multiple workspaces. Think "package manager for repos"
|
|
5
|
+
A CLI tool that manages collections of git repositories across multiple workspaces. Think "package manager for repos" — whether they're open-source projects you learn from or active projects you work on.
|
|
6
6
|
|
|
7
7
|
## Operating Contract
|
|
8
8
|
|
|
@@ -14,14 +14,15 @@ A CLI tool that manages collections of git repositories across multiple workspac
|
|
|
14
14
|
|
|
15
15
|
## Product Standards
|
|
16
16
|
|
|
17
|
-
- Keep CLI,
|
|
17
|
+
- Keep CLI, web dashboard, JSON output, docs, and tests semantically aligned when changing user-facing status behavior.
|
|
18
18
|
- For repo state presentation, avoid using "dirty" as a broad user-facing bucket for every local change. Present it as local/uncommitted changes with the composition visible: modified, staged, and untracked counts.
|
|
19
19
|
- Keep local working-tree state separate from remote relationship state. For example: local changes, clean, ahead, behind, diverged, frozen, missing.
|
|
20
20
|
- When improving the web dashboard, implement the actual missing dashboard feature and shared classification/model behavior instead of copying a CLI-only assumption into the template.
|
|
21
|
+
- For `web/` changes, HTTP-level tests (TestClient, curl) are necessary but not sufficient — they pass on bugs below the HTTP layer. Before calling a `web/` change done, verify form structure against the parsed DOM (browsers silently drop nested `<form>` tags) and verify layout/geometry in a real browser (`overflow-x: auto` clips absolutely-positioned children even without horizontal overflow).
|
|
21
22
|
|
|
22
23
|
## Key Concept: Workspaces
|
|
23
24
|
|
|
24
|
-
Repos are organized into **workspaces**
|
|
25
|
+
Repos are organized into **workspaces** — directories with a label and layout mode:
|
|
25
26
|
- **structured** layout: `workspace/owner/repo/` (open-source collections)
|
|
26
27
|
- **flat** layout: `workspace/repo/` (active projects, no owner subdirectory)
|
|
27
28
|
|
|
@@ -40,60 +41,62 @@ src/gitstow/
|
|
|
40
41
|
│ ├── exec_cmd.py, search.py, open_cmd.py, stats.py # Power commands
|
|
41
42
|
│ ├── export_cmd.py # export/import collection
|
|
42
43
|
│ ├── shell.py # Shell integration (fzf, aliases)
|
|
43
|
-
│ ├── tui.py # TUI launcher
|
|
44
44
|
│ ├── serve.py # Web dashboard launcher
|
|
45
45
|
│ ├── migrate.py, config_cmd.py, onboard.py, doctor.py, skill_cmd.py
|
|
46
46
|
│ └── __init__.py
|
|
47
47
|
├── core/ # Business logic (git ops, URL parsing, state)
|
|
48
|
-
│ ├── paths.py
|
|
49
|
-
│ ├── config.py
|
|
50
|
-
│ ├── repo.py
|
|
51
|
-
│ ├── url_parser.py
|
|
52
|
-
│ ├── git.py
|
|
53
|
-
│ ├── discovery.py
|
|
54
|
-
│ ├── parallel.py
|
|
55
|
-
│
|
|
56
|
-
├──
|
|
57
|
-
│ ├──
|
|
48
|
+
│ ├── paths.py # Path constants, central repos.yaml resolution
|
|
49
|
+
│ ├── config.py # Workspace + Settings dataclasses, load/save
|
|
50
|
+
│ ├── repo.py # Repo dataclass + RepoStore (nested YAML CRUD)
|
|
51
|
+
│ ├── url_parser.py # URL → (host, owner, repo) extraction
|
|
52
|
+
│ ├── git.py # All git subprocess calls
|
|
53
|
+
│ ├── discovery.py # Walk directory tree (structured + flat), reconcile
|
|
54
|
+
│ ├── parallel.py # Async execution with semaphore
|
|
55
|
+
│ ├── status_model.py # Shared repo-state classifier — local composition vs remote relationship
|
|
56
|
+
│ ├── operations.py # Shared filter + bulk-runner layer (pull/fetch/MCP)
|
|
57
|
+
│ ├── locking.py # Cross-process file lock guarding repos.yaml writes
|
|
58
58
|
│ └── __init__.py
|
|
59
59
|
├── web/ # FastAPI browser dashboard (gitstow ui)
|
|
60
60
|
│ ├── server.py # FastAPI app, uvicorn runner, app.state.server stash
|
|
61
61
|
│ ├── static/app.css # Dark theme, Bricolage Grotesque + JetBrains Mono
|
|
62
62
|
│ ├── templates/ # Jinja2 — base.html + page templates + partials/
|
|
63
63
|
│ └── routes/ # dashboard.py, repos.py, workspaces.py, collection.py, pages.py, system.py
|
|
64
|
-
└── skill/ #
|
|
64
|
+
└── skill/ # Claude Code skill (SKILL.md)
|
|
65
65
|
└── SKILL.md
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
**Key rules:**
|
|
69
|
-
1. CLI never touches git directly
|
|
69
|
+
1. CLI never touches git directly — it calls `core/git.py`
|
|
70
70
|
2. `core/repo.py` (RepoStore) is the only module that reads/writes `repos.yaml`
|
|
71
|
-
3. `core/config.py` defines Workspace dataclass
|
|
71
|
+
3. `core/config.py` defines Workspace dataclass — all workspace logic flows from here
|
|
72
72
|
4. `cli/helpers.py` provides `resolve_workspaces()`, `resolve_repo()`, `iter_repos_with_workspace()`
|
|
73
73
|
|
|
74
74
|
## Key Files
|
|
75
75
|
|
|
76
|
-
- `core/config.py`
|
|
77
|
-
- `core/repo.py`
|
|
78
|
-
- `core/discovery.py`
|
|
79
|
-
- `core/url_parser.py`
|
|
80
|
-
- `core/git.py`
|
|
81
|
-
- `core/parallel.py`
|
|
82
|
-
- `
|
|
83
|
-
- `
|
|
84
|
-
- `
|
|
85
|
-
- `
|
|
76
|
+
- `core/config.py` — Workspace + Settings dataclasses. `get_workspaces()` with legacy migration shim.
|
|
77
|
+
- `core/repo.py` — Repo with workspace field, RepoStore with nested YAML format, legacy auto-migration.
|
|
78
|
+
- `core/discovery.py` — `discover_repos(root, layout)` supports structured and flat layouts.
|
|
79
|
+
- `core/url_parser.py` — URL parsing (the hardest part). Test changes here thoroughly.
|
|
80
|
+
- `core/git.py` — All git subprocess calls. Uses `git status --porcelain=v2 --branch` for single-call efficiency.
|
|
81
|
+
- `core/parallel.py` — Async execution with semaphore (max 6 concurrent).
|
|
82
|
+
- `core/status_model.py` — `RepoState` — the single source of truth for local (modified/staged/untracked) vs remote (in-sync/ahead/behind/diverged) classification, consumed by CLI, web, and JSON.
|
|
83
|
+
- `core/operations.py` — Shared `filter_repo_pairs()` + bulk-runner used by `pull`, `fetch`, and the MCP server so surfaces can't drift.
|
|
84
|
+
- `core/locking.py` — `file_lock()` cross-process advisory lock guarding `repos.yaml` against concurrent CLI/web writes.
|
|
85
|
+
- `cli/helpers.py` — Shared workspace resolution used by all CLI commands.
|
|
86
|
+
- `cli/workspace_cmd.py` — workspace list/add/remove/scan subcommands.
|
|
87
|
+
- `cli/main.py` — Typer app, global `-w/--workspace` option, command registration.
|
|
86
88
|
|
|
87
89
|
## Data Files
|
|
88
90
|
|
|
89
|
-
- `~/.gitstow/config.yaml`
|
|
90
|
-
- `~/.gitstow/repos.yaml`
|
|
91
|
+
- `~/.gitstow/config.yaml` — Settings (workspaces list, default host, SSH pref).
|
|
92
|
+
- `~/.gitstow/repos.yaml` — Repo metadata nested by workspace label. Central location.
|
|
93
|
+
- `~/.gitstow/repos.lock` — Cross-process advisory lock file (`core/locking.py`) held during `repos.yaml` writes. Not user-facing data; safe to ignore/delete if orphaned.
|
|
91
94
|
|
|
92
95
|
### repos.yaml format
|
|
93
96
|
```yaml
|
|
94
97
|
oss:
|
|
95
|
-
anthropic/
|
|
96
|
-
remote_url: https://github.com/anthropic/
|
|
98
|
+
anthropic/claude-code:
|
|
99
|
+
remote_url: https://github.com/anthropic/claude-code.git
|
|
97
100
|
tags: [ai]
|
|
98
101
|
active:
|
|
99
102
|
gitstow:
|
|
@@ -101,14 +104,14 @@ active:
|
|
|
101
104
|
tags: [active]
|
|
102
105
|
```
|
|
103
106
|
|
|
104
|
-
## All Commands (
|
|
107
|
+
## All Commands (37)
|
|
105
108
|
|
|
106
109
|
**Core:** `add`, `pull`, `fetch`, `list`, `status`, `remove`, `migrate`
|
|
107
110
|
**Workspace:** `workspace list`, `workspace add`, `workspace remove`, `workspace scan`
|
|
108
111
|
**Repo management:** `repo freeze`, `repo unfreeze`, `repo tag`, `repo untag`, `repo tags`, `repo info`
|
|
109
112
|
**Power:** `exec`, `search`, `open`, `stats`
|
|
110
113
|
**Sharing:** `collection export`, `collection import`
|
|
111
|
-
**Shell:** `shell pick`, `shell init`, `shell completions`, `shell setup`, `
|
|
114
|
+
**Shell:** `shell pick`, `shell init`, `shell completions`, `shell setup`, `ui`
|
|
112
115
|
**Setup:** `onboard`, `config show/set/path/migrate-root`, `doctor`, `install-skill`, `setup-ai`, `update`
|
|
113
116
|
|
|
114
117
|
## Development
|
|
@@ -116,11 +119,13 @@ active:
|
|
|
116
119
|
```bash
|
|
117
120
|
cd ~/labs/projects/gitstow
|
|
118
121
|
pip install -e ".[dev]"
|
|
119
|
-
pytest #
|
|
122
|
+
pytest # full suite — keep green
|
|
120
123
|
ruff check src/
|
|
121
|
-
pip install -e ".[tui]" # For TUI development
|
|
122
124
|
```
|
|
123
125
|
|
|
126
|
+
- **Worktree gotcha:** tests run from a git worktree silently exercise the main checkout (the editable install points at the primary clone). From a worktree, run `PYTHONPATH=<worktree>/src .venv/bin/python -m pytest -q`.
|
|
127
|
+
- **Releasing publishes to PyPI.** `scripts/release.sh X.Y.Z` tags and pushes; the tag triggers a public PyPI publish. Require the user's explicit in-session instruction to release — plan approval or a "let's go" on implementation does not cover it.
|
|
128
|
+
|
|
124
129
|
## Patterns
|
|
125
130
|
|
|
126
131
|
- `--json -j` and `--quiet -q` on all main commands
|
|
@@ -132,19 +137,19 @@ pip install -e ".[tui]" # For TUI development
|
|
|
132
137
|
- asyncio with semaphore for parallel git ops
|
|
133
138
|
- `git status --porcelain=v2 --branch` for single-call status (vs gita's 4-5 calls)
|
|
134
139
|
- Repo.global_key (`workspace:key`) for unique identification across workspaces
|
|
135
|
-
- Legacy format auto-migration (flat repos.yaml
|
|
140
|
+
- Legacy format auto-migration (flat repos.yaml → nested, root_path → workspaces)
|
|
136
141
|
|
|
137
142
|
## AI Integration
|
|
138
143
|
|
|
139
|
-
**Primary:
|
|
140
|
-
- Installed to `~/.
|
|
144
|
+
**Primary: Claude Code skill** (`src/gitstow/skill/SKILL.md`)
|
|
145
|
+
- Installed to `~/.claude/skills/gitstow/` via `gitstow install-skill` or `gitstow onboard`
|
|
141
146
|
- Auto-updates on version bumps (checks `.version` marker on every CLI invocation)
|
|
142
|
-
- Zero context cost when inactive
|
|
143
|
-
-
|
|
147
|
+
- Zero context cost when inactive — only loaded when task matches the skill description
|
|
148
|
+
- Claude runs gitstow CLI commands via Bash — full access to all commands
|
|
144
149
|
|
|
145
150
|
**Optional: MCP server** (`src/gitstow/mcp/server.py`)
|
|
146
|
-
- For non-
|
|
151
|
+
- For non-Claude-Code AI tools (Claude Desktop, Cursor, Windsurf)
|
|
147
152
|
- Install: `pip install gitstow[mcp]`, entry point: `gitstow-mcp`
|
|
148
|
-
-
|
|
153
|
+
- 14 tools + 3 resources, wraps same `core/` modules as CLI
|
|
149
154
|
- **Tradeoff:** MCP tools are always loaded into context (costs tokens even when idle).
|
|
150
155
|
The skill has no such cost. Only use MCP for dedicated repo-management setups.
|
|
@@ -4,6 +4,13 @@ All notable changes to gitstow will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.5.0] - 2026-07-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Move repos between workspaces.** From the dashboard's repo detail page (new Workspace section: pick a target, confirm, done) or the new `gitstow repo move <repo> <workspace>` command. The folder physically relocates on disk following the target workspace's layout (structured `owner/repo` vs flat), the catalog updates in the same locked operation, existing tags are kept and the target's auto-tags merge in, and repos that own linked git worktrees get `git worktree repair` run automatically after the move.
|
|
12
|
+
- **Move safety net.** Collisions are refused before anything is touched (catalog, disk, even dangling symlinks); sources that can't move safely are refused with actionable errors (symlinked repos, linked-worktree gitfiles, relative `.git` symlinks, non-repo directories squatting on a tracked path); catalog keys can never escape their workspace on disk; and if anything fails mid-move, the rollback restores the folder without ever deleting data it can't prove is its own.
|
|
13
|
+
|
|
7
14
|
## [0.4.0] - 2026-07-12
|
|
8
15
|
|
|
9
16
|
### Added
|
|
@@ -107,6 +107,9 @@ pytest # full suite — keep green
|
|
|
107
107
|
ruff check src/
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
- **Worktree gotcha:** tests run from a git worktree silently exercise the main checkout (the editable install points at the primary clone). From a worktree, run `PYTHONPATH=<worktree>/src .venv/bin/python -m pytest -q`.
|
|
111
|
+
- **Releasing publishes to PyPI.** `scripts/release.sh X.Y.Z` tags and pushes; the tag triggers a public PyPI publish. Require the user's explicit in-session instruction to release — plan approval or a "let's go" on implementation does not cover it.
|
|
112
|
+
|
|
110
113
|
## Patterns
|
|
111
114
|
|
|
112
115
|
- `--json -j` and `--quiet -q` on all main commands
|
|
@@ -128,6 +131,7 @@ ruff check src/
|
|
|
128
131
|
- For repo state presentation, avoid using "dirty" as a broad user-facing bucket for every local change. Present it as local/uncommitted changes with the composition visible: modified, staged, and untracked counts.
|
|
129
132
|
- Keep local working-tree state separate from remote relationship state. For example: local changes, clean, ahead, behind, diverged, frozen, missing.
|
|
130
133
|
- When improving the web dashboard, implement the actual missing dashboard feature and shared classification/model behavior instead of copying a CLI-only assumption into the template.
|
|
134
|
+
- For `web/` changes, HTTP-level tests (pytest TestClient, curl) are necessary but not sufficient — they pass on bugs that live below the HTTP layer. Before calling a UI change done, verify form structure against the parsed DOM (browsers silently drop nested `<form>` tags) and verify layout/geometry in a real browser (e.g. `overflow-x: auto` clips absolutely-positioned children even without horizontal overflow).
|
|
131
135
|
|
|
132
136
|
## AI Integration
|
|
133
137
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitstow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A git repository library manager — clone, organize, and maintain collections of repos you learn from
|
|
5
5
|
Project-URL: Homepage, https://github.com/rishmadaan/gitstow
|
|
6
6
|
Project-URL: Repository, https://github.com/rishmadaan/gitstow
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Move repos between workspaces — spec
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-16
|
|
4
|
+
**Status:** approved, pending implementation (Codex, new branch)
|
|
5
|
+
|
|
6
|
+
## What
|
|
7
|
+
|
|
8
|
+
A repo can be reassigned from one workspace to another. The folder physically
|
|
9
|
+
moves on disk to the target workspace's directory, and the catalog
|
|
10
|
+
(`repos.yaml`) updates to match. Surfaced in the web dashboard's repo detail
|
|
11
|
+
page and as a thin CLI command. No such capability exists today in any surface.
|
|
12
|
+
|
|
13
|
+
## Core logic — `move_repo()` in `src/gitstow/core/operations.py`
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
def move_repo(store: RepoStore, settings: Settings, key: str, from_ws: str, to_ws: str) -> Repo
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Raises `ValueError` with a human-readable message on any rule violation; on
|
|
20
|
+
success returns the new `Repo` and has updated disk + catalog.
|
|
21
|
+
|
|
22
|
+
Rules, in order:
|
|
23
|
+
|
|
24
|
+
1. **Resolve:** repo must exist in catalog at `from_ws:key`; target workspace
|
|
25
|
+
label must exist in settings and differ from `from_ws`. Else error.
|
|
26
|
+
2. **Re-key by target layout:**
|
|
27
|
+
- target `flat` → new `owner = ""`, new key = `name`.
|
|
28
|
+
- target `structured` → keep current owner; if owner is empty, parse it
|
|
29
|
+
from `remote_url` via `core/url_parser.py`; if still no owner, refuse:
|
|
30
|
+
a structured workspace files repos under `owner/repo` and discovery
|
|
31
|
+
would never find a bare folder.
|
|
32
|
+
3. **Collision checks (before touching anything):**
|
|
33
|
+
- catalog: no existing repo at `to_ws:new_key`.
|
|
34
|
+
- disk: destination path (per target layout) must not exist.
|
|
35
|
+
4. **Disk move:** if the source folder exists, `shutil.move` it to the
|
|
36
|
+
destination (create the owner parent dir first when structured). If the
|
|
37
|
+
source is missing on disk, skip the move — catalog-only reassignment of
|
|
38
|
+
an already-missing repo is allowed. After a structured-source move, remove
|
|
39
|
+
the now-empty owner directory if empty (ignore errors).
|
|
40
|
+
5. **Catalog update (single locked mutation):** remove old entry, add new
|
|
41
|
+
`Repo` with updated `workspace`/`owner`; preserve `remote_url`, `frozen`,
|
|
42
|
+
`added`, `last_pulled`, `last_fetched`; tags = existing tags plus the
|
|
43
|
+
target workspace's `auto_tags` (deduped, order preserved).
|
|
44
|
+
|
|
45
|
+
Ordering note: validate everything first, then disk move, then catalog write.
|
|
46
|
+
|
|
47
|
+
## Web UI — repo detail page
|
|
48
|
+
|
|
49
|
+
- New section "Workspace" on `_repo_drawer.html` (between Tags and Freeze):
|
|
50
|
+
a `<select name="target">` listing all *other* workspace labels, plus a
|
|
51
|
+
Move button. `data-confirm` message states the folder will move on disk.
|
|
52
|
+
If there is only one workspace, render the section disabled with a hint
|
|
53
|
+
instead of an empty select.
|
|
54
|
+
- New route `POST /repos/{workspace}/{key:path}/move` in
|
|
55
|
+
`src/gitstow/web/routes/repos.py`, matching the existing route patterns
|
|
56
|
+
there (path param style, error handling, redirect style). On success,
|
|
57
|
+
303-redirect to the repo's new detail URL (`/repos/{to_ws}/{new_key}`).
|
|
58
|
+
On `ValueError`, re-render the drawer with the error shown the same way
|
|
59
|
+
other drawer errors are shown.
|
|
60
|
+
- No nested `<form>` tags (browsers silently drop them — verify against
|
|
61
|
+
parsed DOM, per CLAUDE.md).
|
|
62
|
+
|
|
63
|
+
## CLI — `gitstow repo move`
|
|
64
|
+
|
|
65
|
+
- `gitstow repo move <key> <target-workspace>` in `src/gitstow/cli/manage.py`,
|
|
66
|
+
mirroring the `repo tag`/`repo freeze` conventions (helpers for repo
|
|
67
|
+
resolution, Rich output, same error style). Wraps the same `move_repo()`.
|
|
68
|
+
|
|
69
|
+
## Tests
|
|
70
|
+
|
|
71
|
+
- `tests/` — core: structured→flat, flat→structured (owner parsed from URL),
|
|
72
|
+
flat→structured with no derivable owner (refused), disk+catalog collision
|
|
73
|
+
refusals, missing-on-disk catalog-only move, tag merge with auto_tags,
|
|
74
|
+
metadata preserved. Use tmp_path fixtures; no network.
|
|
75
|
+
- Web: TestClient test for the move route (success redirect + error render).
|
|
76
|
+
- CLI: one exercise of `repo move` via the existing CLI test harness pattern.
|
|
77
|
+
- Full suite stays green (`pytest` from the worktree with
|
|
78
|
+
`PYTHONPATH=<worktree>/src`).
|
|
79
|
+
|
|
80
|
+
## Out of scope
|
|
81
|
+
|
|
82
|
+
Bulk/multi-select move, drag-and-drop, moving across hosts, MCP tool parity
|
|
83
|
+
(can wrap `move_repo()` later).
|
|
84
|
+
|
|
85
|
+
Nested-group owners (`group/subgroup/repo`) in structured workspaces land at
|
|
86
|
+
`root/group/subgroup/repo` — the same place `gitstow add` clones them today —
|
|
87
|
+
which two-level structured discovery cannot see (pre-existing limitation
|
|
88
|
+
across add/scan/doctor, flagged in Codex review pass 6; fix is a separate
|
|
89
|
+
discovery feature).
|
|
90
|
+
|
|
91
|
+
## Verification (post-implementation, by the session lead — not Codex)
|
|
92
|
+
|
|
93
|
+
Review the full diff; run the suite; then verify in a real browser per
|
|
94
|
+
CLAUDE.md web standards (form structure in parsed DOM, actual move of a
|
|
95
|
+
scratch repo in `/private/tmp/gitstow-demo`).
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitstow"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "A git repository library manager — clone, organize, and maintain collections of repos you learn from"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -17,6 +17,7 @@ from gitstow.core.git import (
|
|
|
17
17
|
format_size,
|
|
18
18
|
is_git_repo,
|
|
19
19
|
)
|
|
20
|
+
from gitstow.core.operations import move_repo
|
|
20
21
|
from gitstow.core.repo import RepoStore
|
|
21
22
|
from gitstow.core.status_model import classify
|
|
22
23
|
from gitstow.cli.helpers import resolve_repo
|
|
@@ -176,6 +177,36 @@ def remove_tags(
|
|
|
176
177
|
console.print(f" [dim]None of those tags were on {repo.key}.[/dim]")
|
|
177
178
|
|
|
178
179
|
|
|
180
|
+
@manage_app.command("move")
|
|
181
|
+
def move(
|
|
182
|
+
ctx: typer.Context,
|
|
183
|
+
repo_key: str = typer.Argument(help="Repo to move (owner/repo or name)."),
|
|
184
|
+
target: str = typer.Argument(help="Target workspace label."),
|
|
185
|
+
) -> None:
|
|
186
|
+
"""[bold]Move[/bold] a repo to another workspace — relocates its folder on disk.
|
|
187
|
+
|
|
188
|
+
\b
|
|
189
|
+
Examples:
|
|
190
|
+
gitstow repo move anthropic/claude-code active
|
|
191
|
+
gitstow -w oss repo move dupe active
|
|
192
|
+
"""
|
|
193
|
+
settings = load_config()
|
|
194
|
+
store = RepoStore()
|
|
195
|
+
ws_label = (ctx.obj or {}).get("workspace")
|
|
196
|
+
repo, _ = resolve_repo(store, settings, repo_key, ws_label)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
moved = move_repo(store, settings, repo.key, repo.workspace, target)
|
|
200
|
+
except ValueError as exc:
|
|
201
|
+
err_console.print(f"[red]Error:[/red] {exc}")
|
|
202
|
+
raise typer.Exit(code=1)
|
|
203
|
+
|
|
204
|
+
console.print(
|
|
205
|
+
f" [green]✓[/green] Moved {repo.key} → workspace [bold]{target}[/bold] "
|
|
206
|
+
f"(now [cyan]{moved.key}[/cyan])."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
179
210
|
@manage_app.command("tags")
|
|
180
211
|
def list_tags(
|
|
181
212
|
quiet: bool = typer.Option(False, "--quiet", "-q", help="One tag per line."),
|
|
@@ -280,6 +280,17 @@ def get_last_commit(repo_path: Path) -> CommitInfo:
|
|
|
280
280
|
)
|
|
281
281
|
|
|
282
282
|
|
|
283
|
+
def repair_worktrees(repo_path: Path) -> bool:
|
|
284
|
+
"""Run `git worktree repair` from a repo that was just relocated.
|
|
285
|
+
|
|
286
|
+
Linked worktrees keep absolute back-pointers into the main repo's
|
|
287
|
+
.git/worktrees/; after the main repo moves, repair rewrites them.
|
|
288
|
+
Returns True on success.
|
|
289
|
+
"""
|
|
290
|
+
result = _run_git(["worktree", "repair"], cwd=repo_path)
|
|
291
|
+
return result.returncode == 0
|
|
292
|
+
|
|
293
|
+
|
|
283
294
|
def get_branch(repo_path: Path) -> str:
|
|
284
295
|
"""Get the current branch name."""
|
|
285
296
|
result = _run_git(["branch", "--show-current"], cwd=repo_path)
|