gitstow 0.2.6__tar.gz → 0.2.8__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.2.6 → gitstow-0.2.8}/.github/workflows/publish.yml +19 -0
- gitstow-0.2.8/AGENTS.md +150 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/CHANGELOG.md +29 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/CLAUDE.md +9 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/PKG-INFO +1 -1
- gitstow-0.2.8/docs/building/audit-2026-07-06.md +149 -0
- gitstow-0.2.8/docs/building/plans/2026-07-06-wave-1-correctness-safety.md +1301 -0
- gitstow-0.2.8/docs/building/plans/2026-07-06-wave-2-status-model.md +1097 -0
- gitstow-0.2.8/docs/building/plans/2026-07-06-wave-3-structure-polish.md +1690 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/pyproject.toml +1 -1
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/__init__.py +1 -1
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/config_cmd.py +39 -38
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/fetch.py +5 -5
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/manage.py +38 -33
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/onboard.py +15 -6
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/pull.py +12 -11
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/remove.py +11 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/setup_ai.py +4 -4
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/workspace_cmd.py +15 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/git.py +9 -1
- gitstow-0.2.8/src/gitstow/core/locking.py +63 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/repo.py +50 -34
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/url_parser.py +33 -10
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/server.py +30 -1
- gitstow-0.2.8/tests/test_cli.py +266 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/tests/test_config.py +63 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/tests/test_git.py +14 -0
- gitstow-0.2.8/tests/test_locking.py +64 -0
- gitstow-0.2.8/tests/test_onboard.py +122 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/tests/test_repo.py +2 -2
- {gitstow-0.2.6 → gitstow-0.2.8}/tests/test_serve.py +27 -1
- gitstow-0.2.8/tests/test_setup_ai.py +49 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/tests/test_url_parser.py +35 -0
- gitstow-0.2.6/tests/test_cli.py +0 -80
- {gitstow-0.2.6 → gitstow-0.2.8}/.claude/settings.json +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/.github/workflows/ci.yml +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/.gitignore +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/BACKLOG.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/CODE_OF_CONDUCT.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/CONTRIBUTING.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/LICENSE +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/README.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/SECURITY.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/demo.gif +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/demo.tape +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/docs/building/implementation-plan.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/docs/user/commands.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/docs/user/concepts.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/docs/user/configuration.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/docs/user/getting-started.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/scripts/release.sh +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/__main__.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/__init__.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/add.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/doctor.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/exec_cmd.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/export_cmd.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/helpers.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/list_cmd.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/main.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/migrate.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/open_cmd.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/search.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/serve.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/shell.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/skill_cmd.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/stats.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/status.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/tui.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/update.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/__init__.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/config.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/discovery.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/parallel.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/paths.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/mcp/__init__.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/mcp/server.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/skill/SKILL.md +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/tui/__init__.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/tui/app.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/__init__.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/__init__.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/collection.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/dashboard.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/pages.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/repos.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/system.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/workspaces.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/static/app.css +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/_repo_drawer.html +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/add_repo.html +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/base.html +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/dashboard.html +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/partials/dashboard_rows.html +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/partials/fetch_summary.html +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/partials/pull_summary.html +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/partials/repo_row.html +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/settings.html +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/workspaces.html +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/tests/__init__.py +0 -0
- {gitstow-0.2.6 → gitstow-0.2.8}/tests/conftest.py +0 -0
|
@@ -5,7 +5,26 @@ on:
|
|
|
5
5
|
tags: ["v*"]
|
|
6
6
|
|
|
7
7
|
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: pip install -e ".[dev]"
|
|
19
|
+
|
|
20
|
+
- name: Lint
|
|
21
|
+
run: ruff check src/
|
|
22
|
+
|
|
23
|
+
- name: Test
|
|
24
|
+
run: pytest --tb=short
|
|
25
|
+
|
|
8
26
|
publish:
|
|
27
|
+
needs: test
|
|
9
28
|
runs-on: ubuntu-latest
|
|
10
29
|
environment: pypi
|
|
11
30
|
permissions:
|
gitstow-0.2.8/AGENTS.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# gitstow - AI Developer Guide
|
|
2
|
+
|
|
3
|
+
## What is this?
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## Operating Contract
|
|
8
|
+
|
|
9
|
+
- Prefer proper long-term solutions over shortcut patches. If a feature is incomplete in one surface, build the feature into that surface instead of papering over it with wording, partial conditionals, or one-off display logic.
|
|
10
|
+
- Do not recommend quick fixes, temporary patches, or narrow workarounds unless the user explicitly asks for a shortcut.
|
|
11
|
+
- Read the relevant existing code before proposing or editing. Let the current architecture decide where the change belongs.
|
|
12
|
+
- Keep changes scoped to the requested behavior. Avoid unrelated refactors, formatting churn, or drive-by cleanups.
|
|
13
|
+
- Do not revert user changes or generated local state unless the user explicitly asks.
|
|
14
|
+
|
|
15
|
+
## Product Standards
|
|
16
|
+
|
|
17
|
+
- Keep CLI, TUI, web dashboard, JSON output, docs, and tests semantically aligned when changing user-facing status behavior.
|
|
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
|
+
- Keep local working-tree state separate from remote relationship state. For example: local changes, clean, ahead, behind, diverged, frozen, missing.
|
|
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
|
+
|
|
22
|
+
## Key Concept: Workspaces
|
|
23
|
+
|
|
24
|
+
Repos are organized into **workspaces** - directories with a label and layout mode:
|
|
25
|
+
- **structured** layout: `workspace/owner/repo/` (open-source collections)
|
|
26
|
+
- **flat** layout: `workspace/repo/` (active projects, no owner subdirectory)
|
|
27
|
+
|
|
28
|
+
Each workspace can have auto-tags applied to all repos it discovers.
|
|
29
|
+
|
|
30
|
+
## Architecture
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
src/gitstow/
|
|
34
|
+
├── cli/ # Typer commands (thin — delegates to core)
|
|
35
|
+
│ ├── main.py # App entry, command registration, global --workspace flag
|
|
36
|
+
│ ├── helpers.py # Shared workspace resolution, repo lookup, iteration
|
|
37
|
+
│ ├── add.py, pull.py, list_cmd.py, status.py, remove.py # Core commands
|
|
38
|
+
│ ├── workspace_cmd.py # workspace list/add/remove/scan subcommands
|
|
39
|
+
│ ├── manage.py # freeze/unfreeze/tag/untag/info subcommands
|
|
40
|
+
│ ├── exec_cmd.py, search.py, open_cmd.py, stats.py # Power commands
|
|
41
|
+
│ ├── export_cmd.py # export/import collection
|
|
42
|
+
│ ├── shell.py # Shell integration (fzf, aliases)
|
|
43
|
+
│ ├── tui.py # TUI launcher
|
|
44
|
+
│ ├── serve.py # Web dashboard launcher
|
|
45
|
+
│ ├── migrate.py, config_cmd.py, onboard.py, doctor.py, skill_cmd.py
|
|
46
|
+
│ └── __init__.py
|
|
47
|
+
├── core/ # Business logic (git ops, URL parsing, state)
|
|
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
|
+
│ └── __init__.py
|
|
56
|
+
├── tui/ # Textual interactive dashboard
|
|
57
|
+
│ ├── app.py # Main TUI application
|
|
58
|
+
│ └── __init__.py
|
|
59
|
+
├── web/ # FastAPI browser dashboard (gitstow ui)
|
|
60
|
+
│ ├── server.py # FastAPI app, uvicorn runner, app.state.server stash
|
|
61
|
+
│ ├── static/app.css # Dark theme, Bricolage Grotesque + JetBrains Mono
|
|
62
|
+
│ ├── templates/ # Jinja2 — base.html + page templates + partials/
|
|
63
|
+
│ └── routes/ # dashboard.py, repos.py, workspaces.py, collection.py, pages.py, system.py
|
|
64
|
+
└── skill/ # Codex skill (SKILL.md)
|
|
65
|
+
└── SKILL.md
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Key rules:**
|
|
69
|
+
1. CLI never touches git directly - it calls `core/git.py`
|
|
70
|
+
2. `core/repo.py` (RepoStore) is the only module that reads/writes `repos.yaml`
|
|
71
|
+
3. `core/config.py` defines Workspace dataclass - all workspace logic flows from here
|
|
72
|
+
4. `cli/helpers.py` provides `resolve_workspaces()`, `resolve_repo()`, `iter_repos_with_workspace()`
|
|
73
|
+
|
|
74
|
+
## Key Files
|
|
75
|
+
|
|
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
|
+
- `cli/helpers.py` - Shared workspace resolution used by all CLI commands.
|
|
83
|
+
- `cli/workspace_cmd.py` - workspace list/add/remove/scan subcommands.
|
|
84
|
+
- `cli/main.py` - Typer app, global `-w/--workspace` option, command registration.
|
|
85
|
+
- `tui/app.py` - Textual dashboard with DataTable, filter, pull, freeze toggle.
|
|
86
|
+
|
|
87
|
+
## Data Files
|
|
88
|
+
|
|
89
|
+
- `~/.gitstow/config.yaml` - Settings (workspaces list, default host, SSH pref).
|
|
90
|
+
- `~/.gitstow/repos.yaml` - Repo metadata nested by workspace label. Central location.
|
|
91
|
+
|
|
92
|
+
### repos.yaml format
|
|
93
|
+
```yaml
|
|
94
|
+
oss:
|
|
95
|
+
anthropic/Codex:
|
|
96
|
+
remote_url: https://github.com/anthropic/Codex.git
|
|
97
|
+
tags: [ai]
|
|
98
|
+
active:
|
|
99
|
+
gitstow:
|
|
100
|
+
remote_url: https://github.com/rishmadaan/gitstow.git
|
|
101
|
+
tags: [active]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## All Commands (32)
|
|
105
|
+
|
|
106
|
+
**Core:** `add`, `pull`, `fetch`, `list`, `status`, `remove`, `migrate`
|
|
107
|
+
**Workspace:** `workspace list`, `workspace add`, `workspace remove`, `workspace scan`
|
|
108
|
+
**Repo management:** `repo freeze`, `repo unfreeze`, `repo tag`, `repo untag`, `repo tags`, `repo info`
|
|
109
|
+
**Power:** `exec`, `search`, `open`, `stats`
|
|
110
|
+
**Sharing:** `collection export`, `collection import`
|
|
111
|
+
**Shell:** `shell pick`, `shell init`, `shell completions`, `shell setup`, `tui`, `ui`
|
|
112
|
+
**Setup:** `onboard`, `config show/set/path/migrate-root`, `doctor`, `install-skill`, `setup-ai`, `update`
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
cd ~/labs/projects/gitstow
|
|
118
|
+
pip install -e ".[dev]"
|
|
119
|
+
pytest # 44 tests
|
|
120
|
+
ruff check src/
|
|
121
|
+
pip install -e ".[tui]" # For TUI development
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Patterns
|
|
125
|
+
|
|
126
|
+
- `--json -j` and `--quiet -q` on all main commands
|
|
127
|
+
- Global `-w/--workspace` flag filters all commands to a single workspace
|
|
128
|
+
- `cli/helpers.py` for workspace resolution (don't repeat in each command)
|
|
129
|
+
- Rich console for stdout, err_console for stderr
|
|
130
|
+
- Typer with `rich_markup_mode="rich"`, `typer.Context` for global options
|
|
131
|
+
- YAML for persistence (not JSON, not SQLite)
|
|
132
|
+
- asyncio with semaphore for parallel git ops
|
|
133
|
+
- `git status --porcelain=v2 --branch` for single-call status (vs gita's 4-5 calls)
|
|
134
|
+
- Repo.global_key (`workspace:key`) for unique identification across workspaces
|
|
135
|
+
- Legacy format auto-migration (flat repos.yaml -> nested, root_path -> workspaces)
|
|
136
|
+
|
|
137
|
+
## AI Integration
|
|
138
|
+
|
|
139
|
+
**Primary: Codex skill** (`src/gitstow/skill/SKILL.md`)
|
|
140
|
+
- Installed to `~/.Codex/skills/gitstow/` via `gitstow install-skill` or `gitstow onboard`
|
|
141
|
+
- Auto-updates on version bumps (checks `.version` marker on every CLI invocation)
|
|
142
|
+
- Zero context cost when inactive - only loaded when task matches the skill description
|
|
143
|
+
- Codex runs gitstow CLI commands via Bash - full access to all commands
|
|
144
|
+
|
|
145
|
+
**Optional: MCP server** (`src/gitstow/mcp/server.py`)
|
|
146
|
+
- For non-Codex AI tools (Codex Desktop, Cursor, Windsurf)
|
|
147
|
+
- Install: `pip install gitstow[mcp]`, entry point: `gitstow-mcp`
|
|
148
|
+
- 13 tools + 3 resources, wraps same `core/` modules as CLI
|
|
149
|
+
- **Tradeoff:** MCP tools are always loaded into context (costs tokens even when idle).
|
|
150
|
+
The skill has no such cost. Only use MCP for dedicated repo-management setups.
|
|
@@ -4,6 +4,35 @@ 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.2.8] - 2026-07-12
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Pasted GitHub browse URLs now resolve correctly.** URLs like `/tree/...`, `/blob/...`, `/pull/...` now resolve to the repo root instead of being misparsed.
|
|
12
|
+
- **`config migrate-root`** now actually updates workspace paths, and honors the global `-w/--workspace` flag.
|
|
13
|
+
- **`repo freeze/unfreeze/tag/untag`** now honor `-w/--workspace` and report cross-workspace ambiguity clearly instead of guessing.
|
|
14
|
+
- **`pull --json` and `fetch --json`** always emit pure JSON on stdout — no more banners or progress lines interleaved with the payload.
|
|
15
|
+
- **Pull summary** keeps per-workspace identity for same-named frozen repos in different workspaces.
|
|
16
|
+
- **Bulk git operations** no longer hang on credential prompts and are now locale-independent.
|
|
17
|
+
- **`remove --delete`** refuses to delete paths that resolve outside the workspace root.
|
|
18
|
+
- **Workspace labels** are now validated (lowercase alphanumeric, dash, underscore only).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Atomic, cross-process-locked writes** to `repos.yaml`, safe for concurrent CLI and web UI use.
|
|
23
|
+
- **Test gate before PyPI publish** — releases now require a passing test suite.
|
|
24
|
+
|
|
25
|
+
### Security
|
|
26
|
+
|
|
27
|
+
- **Web dashboard CSRF protection.** POST routes now reject cross-origin and DNS-rebinding requests on the localhost UI.
|
|
28
|
+
|
|
29
|
+
## [0.2.7] - 2026-05-28
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`gitstow onboard`** now uses Beaupy's `default_is_yes` confirmation parameter, fixing first-run setup crashes on confirmation prompts.
|
|
34
|
+
- **AI integration setup** now renders MCP warning text with balanced Rich markup during onboarding and `gitstow setup-ai`.
|
|
35
|
+
|
|
7
36
|
## [0.2.5] - 2026-04-19
|
|
8
37
|
|
|
9
38
|
### Added
|
|
@@ -119,6 +119,15 @@ pip install -e ".[tui]" # For TUI development
|
|
|
119
119
|
- Repo.global_key (`workspace:key`) for unique identification across workspaces
|
|
120
120
|
- Legacy format auto-migration (flat repos.yaml → nested, root_path → workspaces)
|
|
121
121
|
|
|
122
|
+
## Product & Implementation Standards
|
|
123
|
+
|
|
124
|
+
- Prefer proper long-term solutions over shortcut patches. If a feature is incomplete in one surface, build the feature into that surface instead of papering over it with wording, partial conditionals, or one-off display logic.
|
|
125
|
+
- Do not recommend quick fixes, temporary patches, or narrow workarounds unless the user explicitly asks for a shortcut.
|
|
126
|
+
- Keep CLI, TUI, web dashboard, JSON output, docs, and tests semantically aligned when changing user-facing status behavior.
|
|
127
|
+
- 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.
|
|
128
|
+
- Keep local working-tree state separate from remote relationship state. For example: local changes, clean, ahead, behind, diverged, frozen, missing.
|
|
129
|
+
- 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.
|
|
130
|
+
|
|
122
131
|
## AI Integration
|
|
123
132
|
|
|
124
133
|
**Primary: Claude Code skill** (`src/gitstow/skill/SKILL.md`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitstow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
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,149 @@
|
|
|
1
|
+
# gitstow — Full Audit (v0.2.7)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-06
|
|
4
|
+
**Auditor:** Claude (Fable 5), full read of all source (~9.5k lines Python), planning docs, tests, CI, packaging
|
|
5
|
+
**Baseline:** commit `1aefe9a`, 139 tests passing
|
|
6
|
+
**Status:** Awaiting greenlight — see [Implementation Waves](#implementation-waves)
|
|
7
|
+
|
|
8
|
+
This document is the working tracker for the post-audit improvement effort. Each finding
|
|
9
|
+
has an ID for reference. Check items off as waves land.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Planned but never done
|
|
14
|
+
|
|
15
|
+
Commitments in `docs/building/implementation-plan.md` and `CLAUDE.md` that the code does not fulfill.
|
|
16
|
+
|
|
17
|
+
| ID | Finding | Where |
|
|
18
|
+
|----|---------|-------|
|
|
19
|
+
| P1 | **Parallel `add` clones.** Plan: "Multiple URLs are cloned concurrently (semaphore = parallel_limit)." Implementation is a sequential for-loop. | `cli/add.py:105` |
|
|
20
|
+
| P2 | **Remote-mismatch conflict detection.** Plan: existing dir with a different remote → "error, explain conflict". Instead `add` silently registers whatever is on disk, even when its remote differs from the requested URL. | `cli/add.py:141` |
|
|
21
|
+
| P3 | **Reconciliation on `list`/`status`/`pull`.** Plan design decision #2: reconcile disk-vs-store on these commands and show untracked repos as "untracked" with a hint. Only `doctor` reconciles. | `cli/list_cmd.py`, `cli/status.py`, `cli/pull.py` |
|
|
22
|
+
| P4 | **The status-presentation standard (CLAUDE.md "Product & Implementation Standards") is implemented on no surface.** The standard: don't use "dirty" as a broad bucket; show modified/staged/untracked composition; keep local working-tree state separate from remote relationship state. Reality: CLI shows combined `dirty(N)`; web `_classify` only reads `status.dirty` so **staged-only or untracked-only repos display as "clean"**; TUI computes dirty as `total − clean − frozen`. Three divergent classifiers. | `cli/status.py:168`, `web/routes/dashboard.py:62`, `tui/app.py:224` |
|
|
23
|
+
| P5 | **TUI acknowledged broken** (BACKLOG.md) and parked. Decision needed: triage or retire. | `tui/app.py` |
|
|
24
|
+
| P6 | **CHANGELOG missing the 0.2.6 entry.** `release.sh` bumps versions but never enforces a changelog entry. | `CHANGELOG.md`, `scripts/release.sh` |
|
|
25
|
+
|
|
26
|
+
## 2. Correctness bugs
|
|
27
|
+
|
|
28
|
+
| ID | Severity | Finding | Where |
|
|
29
|
+
|----|----------|---------|-------|
|
|
30
|
+
| B1 | High | **Deep URLs parse to garbage** (verified live). `github.com/owner/repo/tree/main/src` → owner `owner/repo/tree/main`, repo `src`, clone URL `.../tree/main/src.git`. Any pasted `/tree/`, `/blob/`, `/pull/`, `/issues/` link — the most common real paste — creates a bogus nested dir and failing clone. Nested-group (GitLab) handling needs host-aware guards. | `core/url_parser.py:134` |
|
|
31
|
+
| B2 | High | **`config migrate-root` broken post-workspaces.** Sets legacy `settings.root_path`, but `Settings.to_dict()` drops `root_path` whenever workspaces exist, and workspace paths are never updated. Files move; config still points at old location; all repos show "missing". `config set` docstring still advertises `root_path`, which the command rejects. | `cli/config_cmd.py:223`, `core/config.py:86` |
|
|
32
|
+
| B3 | High | **repos.yaml writes are neither atomic nor locked.** Advertised workflow is CLI + web UI running simultaneously; concurrent load-modify-save loses updates; crash mid-write corrupts the file. Needs temp-file + `os.replace` + file lock. | `core/repo.py:149` |
|
|
33
|
+
| B4 | High | **No `GIT_TERMINAL_PROMPT=0` / `LC_ALL=C` on git subprocesses.** One auth-prompting repo hangs every bulk pull for the full timeout; "Already up to date" string-match breaks on non-English locales. | `core/git.py:83`, `core/git.py:160` |
|
|
34
|
+
| B5 | Med | **`repo freeze/unfreeze/tag/untag` ignore `-w` and mishandle ambiguity.** They call `store.get(key)` directly; same key in two workspaces → `_resolve_global_key` returns `None` → misleading "not tracked". `freeze --tag` updates by bare key and can hit the wrong workspace's repo. | `cli/manage.py:62,54` |
|
|
35
|
+
| B6 | Med | **Web pull-all pulls dirty repos; CLI never does.** Cross-surface semantic mismatch (ff-only limits damage). | `web/routes/repos.py:153` vs `cli/pull.py:35` |
|
|
36
|
+
| B7 | Med | **`collection import` loses workspace assignment and fails silently on newer versions.** Exports carry per-repo `workspace`; import dumps everything into one workspace. Newer-version file → bare `typer.Exit(1)` with a "caller should print error" comment; nothing is printed. | `cli/export_cmd.py:189,258` |
|
|
37
|
+
| B8 | Med | **`workspace remove` silently orphans repos.** Default `--keep-repos` leaves repos.yaml entries pointing at a nonexistent workspace — invisible to `list`/`status`/web; `doctor` doesn't flag them either. | `cli/workspace_cmd.py:118` |
|
|
38
|
+
| B9 | Low | **`open --editor` is a dead flag**; editor detection prefers `code`/`cursor` over `$EDITOR`; terminal `$EDITOR` (vim) launched via `Popen` with no TTY. | `cli/open_cmd.py:24,116` |
|
|
39
|
+
| B10 | Low | **`pull` frozen bookkeeping uses `r.key` not `global_key`** — same-named repos in two workspaces collapse into one "frozen" summary line. | `cli/pull.py:112` |
|
|
40
|
+
| B11 | Low | **No label validation on `workspace add`** — `:` or `/` in a label breaks `global_key` parsing and web routes. | `cli/workspace_cmd.py:67` |
|
|
41
|
+
| B12 | Low | **CLI `remove --delete` has no path-containment check**; web delete route defensively verifies path is under workspace root. CLI should get the same guard. | `cli/remove.py:66` vs `web/routes/repos.py:377` |
|
|
42
|
+
| B13 | Low | **`_resolve_global_key` dead code**: final line re-tests `len(matches) == 1` after that branch already returned; ambiguous vs missing indistinguishable to callers. | `core/repo.py:215` |
|
|
43
|
+
|
|
44
|
+
## 3. Efficiency
|
|
45
|
+
|
|
46
|
+
| ID | Finding | Where |
|
|
47
|
+
|----|---------|-------|
|
|
48
|
+
| E1 | **Web dashboard status is sequential AND blocks the event loop.** `_build_repos_data` runs `get_status` (subprocess) per repo, serially, inside `async def`, on every render + every 30s auto-refresh. 100 repos ≈ multi-second loads. `core/parallel.run_parallel` exists and is used for pulls — not for status. | `web/routes/dashboard.py:122` |
|
|
49
|
+
| E2 | **`store.update()` rewrites the whole YAML per repo.** Pull-all over 50 repos = 50 full-file rewrites (CLI, web, fetch all do this in loops). Needs batch-update / deferred save. | `core/repo.py:259` + callers |
|
|
50
|
+
| E3 | **`add` clones sequentially** (also P1). | `cli/add.py:105` |
|
|
51
|
+
| E4 | **`search` is sequential** across repos; `exec` is parallel. | `cli/search.py:82` |
|
|
52
|
+
| E5 | **`get_disk_size` rglobs every file in Python** (`stats`, `repo info`) — very slow on large repos. Use `du -sk` or parallelize. | `core/git.py:280` |
|
|
53
|
+
| E6 | **Clone timeout hardcoded 300s** — kernel-sized repos fail by design; not configurable. | `core/git.py:141` |
|
|
54
|
+
|
|
55
|
+
## 4. Architecture
|
|
56
|
+
|
|
57
|
+
| ID | Finding | Notes |
|
|
58
|
+
|----|---------|-------|
|
|
59
|
+
| A1 | **Status classification exists in 3 divergent copies** (CLI, web `_classify`, TUI). Fix for P4 is a single `core/status_model.py` classifier consumed by all surfaces + JSON. Highest-leverage refactor in the codebase. | |
|
|
60
|
+
| A2 | **Web stack (fastapi, uvicorn, jinja2, python-multipart) ships as core deps** for every install, including pure-CLI users. `cli/serve.py` already handles ImportError gracefully, so a `[ui]` extra is nearly free. Deliberate v0.2.0 choice — revisit (tradeoff: onboarding friction vs install weight). | Decision needed |
|
|
61
|
+
| A3 | **MCP server (704 lines) reimplements add/pull/list** instead of sharing an operations layer → drift risk (already lacks `last_fetched`; `list --json` omits it too, so no JSON consumer can see fetch state). | `mcp/server.py`, `cli/list_cmd.py:91` |
|
|
62
|
+
| A4 | **~300 lines of copy-paste across CLI commands**: `pull.py`/`fetch.py` retry+progress+summary loops nearly identical; the same tag/owner/frozen filter block appears in six commands. Natural home: `core/operations.py` (filters + bulk-runner), which is also what web/MCP should call. | |
|
|
63
|
+
|
|
64
|
+
## 5. Security
|
|
65
|
+
|
|
66
|
+
| ID | Severity | Finding | Where |
|
|
67
|
+
|----|----------|---------|-------|
|
|
68
|
+
| S1 | High | **Web UI has zero CSRF protection.** All mutations are POSTs; any website can fire cross-origin form POSTs at `http://127.0.0.1:7853` (delete repos from disk, add attacker-URL repos, shutdown). Localhost binding does not prevent this. Fix: reject requests whose `Origin`/`Host` isn't localhost (or session token). The stated design goal "arbitrary git execution must not be LAN-reachable" has this gap. | `web/routes/*.py` |
|
|
69
|
+
|
|
70
|
+
## 6. Deployment & CI
|
|
71
|
+
|
|
72
|
+
| ID | Finding | Where |
|
|
73
|
+
|----|---------|-------|
|
|
74
|
+
| D1 | **`publish.yml` ships to PyPI without running tests.** Tag push → build → publish; CI is not a gate. Add test job as dependency of publish. | `.github/workflows/publish.yml` |
|
|
75
|
+
| D2 | **CI ubuntu-only** while classifiers claim macOS + Windows and development happens on macOS. Add macOS to matrix at minimum. | `.github/workflows/ci.yml` |
|
|
76
|
+
| D3 | **`release.sh` doesn't require a CHANGELOG entry** (proven by missing 0.2.6). | `scripts/release.sh` |
|
|
77
|
+
| D4 | **`typer[all]` is a deprecated extra** in modern Typer (harmless noise). | `pyproject.toml:33` |
|
|
78
|
+
| D5 | **No tests for `fetch` CLI, MCP server, or TUI.** | `tests/` |
|
|
79
|
+
|
|
80
|
+
## 7. Docs drift
|
|
81
|
+
|
|
82
|
+
| ID | Finding |
|
|
83
|
+
|----|---------|
|
|
84
|
+
| DOC1 | CLAUDE.md says "44 tests" (actual 139); command count worth re-verifying after changes. |
|
|
85
|
+
| DOC2 | BACKLOG.md references `.github/workflows/release.yml`; actual file is `publish.yml`. |
|
|
86
|
+
| DOC3 | `list --json` omits `last_fetched` though tracked (also A3). |
|
|
87
|
+
| DOC4 | `config set` help/docstring advertises `root_path`, which is rejected (also B2). |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Implementation Waves
|
|
92
|
+
|
|
93
|
+
Execution model per repo convention: Fable designs/reviews; implementation delegated to
|
|
94
|
+
Opus (substantial) / Sonnet (mechanical) subagents.
|
|
95
|
+
|
|
96
|
+
**Detailed task-by-task plans (TDD steps, code, commands):**
|
|
97
|
+
- Wave 1: [plans/2026-07-06-wave-1-correctness-safety.md](plans/2026-07-06-wave-1-correctness-safety.md)
|
|
98
|
+
- Wave 2: [plans/2026-07-06-wave-2-status-model.md](plans/2026-07-06-wave-2-status-model.md)
|
|
99
|
+
- Wave 3: [plans/2026-07-06-wave-3-structure-polish.md](plans/2026-07-06-wave-3-structure-polish.md)
|
|
100
|
+
|
|
101
|
+
**Product decisions locked 2026-07-06:**
|
|
102
|
+
1. Web stack moves to an optional `[ui]` extra (ships in 0.3.0) — A2.
|
|
103
|
+
2. The broken Textual TUI is retired, not repaired (moved to Wave 2 Task 1 so the
|
|
104
|
+
status model only needs two consumer surfaces) — P5.
|
|
105
|
+
3. Bulk pull skips only modified/staged repos; untracked-only repos ARE pulled
|
|
106
|
+
(CLI + web adopt the same rule) — informs B6/P4.
|
|
107
|
+
|
|
108
|
+
**Release strategy:** Wave 1 → 0.2.8 (patch). Waves 2 + 3 → 0.3.0 together
|
|
109
|
+
(TUI removal + `[ui]` extra are the breaking changes justifying the minor bump).
|
|
110
|
+
|
|
111
|
+
### Wave 1 — Correctness & safety (Opus) — COMPLETE (2026-07-12)
|
|
112
|
+
- [x] B1 deep-URL parsing guards (host-aware; GitHub/known hosts reject >2 path segments, strip `/tree|blob|pull|issues/...`)
|
|
113
|
+
- [x] B3 atomic writes (temp + `os.replace`) + file locking on repos.yaml
|
|
114
|
+
- [x] B4 `GIT_TERMINAL_PROMPT=0`, `LC_ALL=C` in `_run_git`; drop locale-dependent string matching where possible
|
|
115
|
+
- [x] B2 fix or explicitly retire `config migrate-root` for the workspace era (+ DOC4)
|
|
116
|
+
- [x] B5 route `repo freeze/unfreeze/tag/untag` through `resolve_repo` with `-w` support; distinguish ambiguous from missing (B13)
|
|
117
|
+
- [x] S1 Origin/Host check middleware on all POST routes
|
|
118
|
+
- [x] D1 test gate before PyPI publish
|
|
119
|
+
- [x] B12 path-containment check in CLI `remove --delete`
|
|
120
|
+
- [x] B11 workspace label validation
|
|
121
|
+
|
|
122
|
+
### Wave 2 — Shared status model (Opus) — NOT STARTED
|
|
123
|
+
- [ ] P5 retire the Textual TUI (decision: retire; clears the third surface before the refactor)
|
|
124
|
+
- [ ] A1/P4 `core/status_model.py`: one classifier separating local composition (modified/staged/untracked counts) from remote relationship (in-sync/ahead/behind/diverged) + frozen/missing overlays
|
|
125
|
+
- [ ] Consume from CLI `status`, web dashboard, and `--json` payloads (per CLAUDE.md standards)
|
|
126
|
+
- [ ] B6 one pull rule on both surfaces: modified/staged skip, untracked-only pulls (decision locked)
|
|
127
|
+
- [ ] E1 parallelize web status gathering via `run_parallel` (off the event loop)
|
|
128
|
+
- [ ] E2 batch RepoStore updates — `bulk()` (single save per bulk op)
|
|
129
|
+
- [ ] Tests covering the classifier matrix
|
|
130
|
+
|
|
131
|
+
### Wave 3 — Structure & polish (Sonnet-heavy, Opus for A3/A4) — NOT STARTED
|
|
132
|
+
- [ ] P1/E3 parallel `add` clones; P2 remote-mismatch conflict error
|
|
133
|
+
- [ ] E4 parallel `search`
|
|
134
|
+
- [ ] A4 `core/operations.py` extraction (filters + bulk runner); collapse pull/fetch duplication
|
|
135
|
+
- [ ] A3 MCP server consumes operations layer; add `last_fetched` everywhere (DOC3); new `fetch_repos` tool
|
|
136
|
+
- [ ] P3 reconciliation hints surfacing untracked repos in `list`/`status` (human output only; JSON shapes unchanged)
|
|
137
|
+
- [ ] B7 import preserves workspace; loud version error. B8 orphaned-workspace detection in doctor + workspace remove warning
|
|
138
|
+
- [ ] B9 `$VISUAL`/`$EDITOR`-first open logic; terminal editors run foreground
|
|
139
|
+
- [ ] A2 web stack → `[ui]` extra (decision locked; ships 0.3.0)
|
|
140
|
+
- [ ] E5 `du`-backed disk sizing; E6 configurable `clone_timeout` setting
|
|
141
|
+
- [ ] D2 macOS CI; D3 changelog gate in release.sh; D4 typer extra; D5 fetch/MCP tests
|
|
142
|
+
- [ ] DOC1/DOC2 docs sync
|
|
143
|
+
|
|
144
|
+
## Verification notes
|
|
145
|
+
|
|
146
|
+
- All 139 tests pass at baseline (`pytest -q`, 2.82s).
|
|
147
|
+
- B1 verified by executing the parser against deep URLs.
|
|
148
|
+
- B2 verified by reading `Settings.to_dict()` serialization guard (`core/config.py:86`).
|
|
149
|
+
- B3/B4/S1 verified by code inspection; no runtime exploit performed.
|