gitstow 0.7.1__tar.gz → 0.7.2__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.7.1 → gitstow-0.7.2}/.gitignore +2 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/AGENTS.md +2 -2
- {gitstow-0.7.1 → gitstow-0.7.2}/CHANGELOG.md +13 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/CLAUDE.md +6 -3
- {gitstow-0.7.1 → gitstow-0.7.2}/PKG-INFO +9 -3
- {gitstow-0.7.1 → gitstow-0.7.2}/README.md +8 -2
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/user/commands.md +36 -3
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/user/concepts.md +13 -2
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/user/configuration.md +33 -1
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/user/getting-started.md +41 -9
- {gitstow-0.7.1 → gitstow-0.7.2}/pyproject.toml +1 -1
- {gitstow-0.7.1 → gitstow-0.7.2}/site/index.html +2 -2
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/__init__.py +1 -1
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/add.py +4 -3
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/config_cmd.py +10 -11
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/doctor.py +28 -10
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/exec_cmd.py +3 -1
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/fetch.py +21 -4
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/helpers.py +107 -9
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/list_cmd.py +3 -1
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/migrate.py +3 -2
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/onboard.py +12 -3
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/pull.py +21 -4
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/search.py +3 -1
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/stats.py +3 -1
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/status.py +3 -1
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/workspace_cmd.py +13 -8
- gitstow-0.7.2/src/gitstow/core/config.py +251 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/paths.py +2 -1
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/mcp/server.py +28 -2
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/skill/SKILL.md +24 -3
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/collection.py +10 -2
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/pages.py +5 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/repos.py +37 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/add_repo.html +10 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/dashboard.html +12 -0
- gitstow-0.7.2/src/gitstow/web/templates/partials/no_workspaces.html +17 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/settings.html +8 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/workspaces.html +6 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/conftest.py +13 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_cli.py +350 -2
- gitstow-0.7.2/tests/test_config.py +567 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_mcp.py +37 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_onboard.py +126 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_serve.py +249 -16
- gitstow-0.7.1/src/gitstow/core/config.py +0 -144
- gitstow-0.7.1/tests/test_config.py +0 -245
- {gitstow-0.7.1 → gitstow-0.7.2}/.claude/settings.json +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/.github/workflows/ci.yml +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/.github/workflows/pages.yml +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/.github/workflows/publish.yml +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/BACKLOG.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/CODE_OF_CONDUCT.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/CONTRIBUTING.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/LICENSE +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/SECURITY.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/demo.gif +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/demo.tape +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/Gitstow Landing.dc.html +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/README.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/assets/demo.gif +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/assets/fonts/bricolage-grotesque-latin-var.woff2 +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/assets/fonts/jetbrains-mono-latin-var.woff2 +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/browser-window.jsx +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/01-hero.png +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/02-quick-start.png +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/03-dashboard-showcase.png +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/04-ai-integration.png +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/05-feature-grid.png +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/06-comparison.png +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/design_handoff_gitstow_landing/screenshots/07-cta-footer.png +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/audit-2026-07-06.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/implementation-plan.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-06-wave-1-correctness-safety.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-06-wave-2-status-model.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-06-wave-3-structure-polish.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-12-ui-wave-a-make-it-real.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-12-ui-wave-b-polish.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/plans/2026-07-16-move-repo-between-workspaces.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/roadmap.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/building/ui-audit-2026-07-12.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/superpowers/plans/2026-07-19-diff-viewer.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/superpowers/plans/2026-08-11-tailscale-ui.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/superpowers/specs/2026-07-19-diff-viewer-design.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/docs/superpowers/specs/2026-08-11-tailscale-ui-design.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/scripts/release.sh +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/site/assets/demo.gif +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/site/assets/fonts/bricolage-grotesque-latin-var.woff2 +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/site/assets/fonts/jetbrains-mono-latin-var.woff2 +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/site/style.css +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/__main__.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/__init__.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/diff_cmd.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/export_cmd.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/main.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/manage.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/open_cmd.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/remove.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/serve.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/setup_ai.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/shell.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/skill_cmd.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/cli/update.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/__init__.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/collection_io.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/diff.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/discovery.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/git.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/locking.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/operations.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/parallel.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/repo.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/status_model.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/tailscale.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/core/url_parser.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/mcp/__init__.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/__init__.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/__init__.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/dashboard.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/system.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/routes/workspaces.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/server.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/app.css +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/dashboard.js +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/fonts/bricolage-grotesque-latin-ext-var.woff2 +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/fonts/bricolage-grotesque-latin-var.woff2 +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/fonts/fonts.css +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/fonts/jetbrains-mono-latin-ext-var.woff2 +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/fonts/jetbrains-mono-latin-var.woff2 +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/vendor/VENDORED.md +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/static/vendor/htmx.min.js +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/_repo_drawer.html +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/base.html +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/changes_section.html +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/dashboard_rows.html +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/diff_view.html +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/fetch_summary.html +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/pull_summary.html +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/src/gitstow/web/templates/partials/repo_row.html +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/__init__.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_collection_io.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_diff.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_git.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_locking.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_move.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_operations.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_repo.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_setup_ai.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_status_model.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_tailscale.py +0 -0
- {gitstow-0.7.1 → gitstow-0.7.2}/tests/test_url_parser.py +0 -0
|
@@ -73,7 +73,7 @@ src/gitstow/
|
|
|
73
73
|
|
|
74
74
|
## Key Files
|
|
75
75
|
|
|
76
|
-
- `core/config.py` — Workspace + Settings dataclasses. `get_workspaces()`
|
|
76
|
+
- `core/config.py` — Workspace + Settings dataclasses. `get_workspaces()` returns exactly what is configured and **never synthesizes a workspace**; zero workspaces is a valid state and `get_default_workspace()` returns `None` for it. The legacy `root_path` → `oss` migration shim lives only in `load_config()`, where it is persisted to disk. Both one-time migrations (`root_path` → `oss`, and adopting the pre-0.7.2 implicit `oss` workspace at `~/opensource`) run there too, gated by `config_version` — the provenance marker every save writes, which is what keeps an explicit `workspaces: []` from being re-migrated into a workspace the user just removed. `NO_WORKSPACES_HINT` is the one empty-state message every surface (CLI, MCP, web) reuses.
|
|
77
77
|
- `core/repo.py` — Repo with workspace field, RepoStore with nested YAML format, legacy auto-migration.
|
|
78
78
|
- `core/discovery.py` — `discover_repos(root, layout)` supports structured and flat layouts.
|
|
79
79
|
- `core/url_parser.py` — URL parsing (the hardest part). Test changes here thoroughly.
|
|
@@ -144,7 +144,7 @@ ruff check src/
|
|
|
144
144
|
- asyncio with semaphore for parallel git ops
|
|
145
145
|
- `git status --porcelain=v2 --branch` for single-call status (vs gita's 4-5 calls)
|
|
146
146
|
- Repo.global_key (`workspace:key`) for unique identification across workspaces
|
|
147
|
-
- Legacy format auto-migration (flat repos.yaml → nested, root_path → workspaces)
|
|
147
|
+
- Legacy format auto-migration (flat repos.yaml → nested, root_path → workspaces) — migration happens in `load_config()` and is written to disk, never synthesized on read
|
|
148
148
|
|
|
149
149
|
## AI Integration
|
|
150
150
|
|
|
@@ -4,6 +4,19 @@ 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.7.2] - 2026-09-03
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **A fresh install no longer invents an `oss` workspace at `~/opensource`** — with nothing configured, gitstow used to conjure a workspace that lived only in memory. It could not be removed (`workspace remove oss` refused: "Cannot remove the only workspace"), it could not be replaced (`workspace add ... --label oss` refused: "already exists"), removing it from the dashboard silently did nothing because the page re-conjured it on the next render, and `gitstow add owner/repo` quietly cloned into `~/opensource` under a workspace label that appeared nowhere in your config. Zero workspaces is now a real, visible state: `get_workspaces()` returns exactly what you configured, every surface shows the same empty state with the same one-line fix (`gitstow workspace add <path> --label <name>`, or `gitstow onboard`), and you may remove your last workspace — the CLI tells you what to do next, and the dashboard's library, add-repo, and workspaces pages each show an empty-state card instead of a dead form or a phantom row. The legacy `root_path` → `oss` migration is unchanged and still runs, but only in `load_config()` where it is written to disk, so what you see is what your config holds. `~/opensource` survives solely as the path `gitstow onboard` suggests for your first workspace.
|
|
12
|
+
- **The empty state now reaches every surface that needed it** — `gitstow onboard` works on a config with zero workspaces (it used to refuse because the *file* existed, and `--force` silently reset `parallel_limit`, `clone_timeout` and `ui_tailscale`); `gitstow pull owner/repo` and `gitstow fetch owner/repo` stop with the hint instead of "No repos to pull"; the MCP tools `list_repos`, `pull_repos`, `fetch_repos`, `search_repos`, `collection_stats` and `repo_status` return the hint instead of a success-shaped empty result; the dashboard's Settings page shows the empty-state card in place of the collection-import form, and a POST to it answers with that page rather than raw JSON; the library page no longer renders its "Pull all" / "Fetch all" bar and repo filters with no workspace to act on, and those endpoints answer with the empty-state card instead of a zero-item summary or a wall of "workspace not found"; `gitstow doctor` prints workspace labels in its orphan reports again (Rich was parsing `[label]` as a style tag and swallowing it) and skips the SSH probe when nothing is configured; and the terminal hint prints each command on its own line so it never wraps mid-command.
|
|
13
|
+
- **`--json` runs now get the empty state as JSON, not silence** — with zero workspaces configured, `add`, `pull`, `fetch`, `list`, `status`, `exec`, `search`, `stats` and `migrate` exited 1 with an empty stdout and the hint as prose on stderr, so anything parsing stdout (scripts, the Claude Code skill) saw a crash rather than "nothing configured yet". With `--json` they now write `{"success": false, "error": "No workspaces configured…"}` to stdout, in the same shape as every other JSON failure, and still exit 1. Without `--json` nothing changes.
|
|
14
|
+
- **One orphaned repo no longer aborts a bulk pull or fetch** — `gitstow pull good orphan`, where `orphan` is tracked under a workspace that has since left your config, exited 1 while building the target list and never touched `good`. It now warns and skips that one repo — the same contract as the existing "not tracked. Skipping." path — so the valid repos named alongside it still run. In `--json` mode the warning goes to stderr and stdout stays the result payload. Naming only the orphan still ends in "No repos to pull."
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Upgrading from 0.7.1 or earlier adopts your implicit `oss` workspace** — installs where `gitstow add` had cloned into `~/opensource` without ever writing a config now get that workspace written to `config.yaml` once, silently, on the next command, so those repos stay visible (if `~/opensource` no longer exists, nothing is invented and `gitstow doctor` reports the leftover records). The adoption only ever looks at configs that predate this version — every config gitstow writes from now on carries a `config_version` marker, so once you remove `oss` yourself it stays removed.
|
|
19
|
+
|
|
7
20
|
## [0.7.1] - 2026-08-11
|
|
8
21
|
|
|
9
22
|
### Added
|
|
@@ -39,6 +39,7 @@ src/gitstow/
|
|
|
39
39
|
│ ├── status_model.py # Shared repo-state classifier — local composition vs remote relationship
|
|
40
40
|
│ ├── operations.py # Shared filter + bulk-runner layer (pull/fetch/MCP)
|
|
41
41
|
│ ├── locking.py # Cross-process file lock guarding repos.yaml writes
|
|
42
|
+
│ ├── tailscale.py # Tailscale CLI detection (tailnet IP + MagicDNS) for ui --tailscale
|
|
42
43
|
│ └── __init__.py
|
|
43
44
|
├── web/ # FastAPI browser dashboard (gitstow ui)
|
|
44
45
|
│ ├── server.py # FastAPI app, uvicorn runner, app.state.server stash
|
|
@@ -57,7 +58,7 @@ src/gitstow/
|
|
|
57
58
|
|
|
58
59
|
## Key Files
|
|
59
60
|
|
|
60
|
-
- `core/config.py` — Workspace + Settings dataclasses. `get_workspaces()`
|
|
61
|
+
- `core/config.py` — Workspace + Settings dataclasses. `get_workspaces()` returns exactly what is configured and **never synthesizes a workspace**; zero workspaces is a valid state and `get_default_workspace()` returns `None` for it. The legacy `root_path` → `oss` migration shim lives only in `load_config()`, where it is persisted to disk. Both one-time migrations (`root_path` → `oss`, and adopting the pre-0.7.2 implicit `oss` workspace at `~/opensource`) run there too, gated by `config_version` — the provenance marker every save writes, which is what keeps an explicit `workspaces: []` from being re-migrated into a workspace the user just removed. `NO_WORKSPACES_HINT` is the one empty-state message every surface (CLI, MCP, web) reuses.
|
|
61
62
|
- `core/repo.py` — Repo with workspace field, RepoStore with nested YAML format, legacy auto-migration.
|
|
62
63
|
- `core/discovery.py` — `discover_repos(root, layout)` supports structured and flat layouts.
|
|
63
64
|
- `core/url_parser.py` — URL parsing (the hardest part). Test changes here thoroughly.
|
|
@@ -66,13 +67,14 @@ src/gitstow/
|
|
|
66
67
|
- `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.
|
|
67
68
|
- `core/operations.py` — Shared `filter_repo_pairs()` + bulk-runner used by `pull`, `fetch`, and the MCP server so surfaces can't drift.
|
|
68
69
|
- `core/locking.py` — `file_lock()` cross-process advisory lock guarding `repos.yaml` against concurrent CLI/web writes.
|
|
70
|
+
- `core/tailscale.py` — `detect_tailscale()` / `tailscale_available()`. Shells out to the `tailscale` CLI (3s timeouts, None on any failure). Powers `gitstow ui --tailscale`: `web/server.py` binds the tailnet IP as a second socket and widens the Host/Origin guard by exact match to the machine's own tailnet identity only — never `0.0.0.0`. Every failure mode degrades to localhost-only with a warning.
|
|
69
71
|
- `cli/helpers.py` — Shared workspace resolution used by all CLI commands.
|
|
70
72
|
- `cli/workspace_cmd.py` — workspace list/add/remove/scan subcommands.
|
|
71
73
|
- `cli/main.py` — Typer app, global `-w/--workspace` option, command registration.
|
|
72
74
|
|
|
73
75
|
## Data Files
|
|
74
76
|
|
|
75
|
-
- `~/.gitstow/config.yaml` — Settings (workspaces list, default host, SSH pref).
|
|
77
|
+
- `~/.gitstow/config.yaml` — Settings (workspaces list, default host, SSH pref, `ui_tailscale` dashboard-over-tailnet toggle).
|
|
76
78
|
- `~/.gitstow/repos.yaml` — Repo metadata nested by workspace label. Central location.
|
|
77
79
|
- `~/.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.
|
|
78
80
|
|
|
@@ -130,7 +132,8 @@ ruff check src/
|
|
|
130
132
|
- asyncio with semaphore for parallel git ops
|
|
131
133
|
- `git status --porcelain=v2 --branch` for single-call status (vs gita's 4-5 calls)
|
|
132
134
|
- Repo.global_key (`workspace:key`) for unique identification across workspaces
|
|
133
|
-
- Legacy format auto-migration (flat repos.yaml → nested, root_path → workspaces)
|
|
135
|
+
- Legacy format auto-migration (flat repos.yaml → nested, root_path → workspaces) — migration happens in `load_config()` and is written to disk, never synthesized on read
|
|
136
|
+
- `app.state.tailscale_serving` — `run()` records what it actually bound; the settings page compares saved config against that instead of guessing, so restart notices state facts (and direction), never implied live changes
|
|
134
137
|
|
|
135
138
|
## Product & Implementation Standards
|
|
136
139
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gitstow
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: A git repository library manager — clone, organize, and maintain collections of repos you learn from
|
|
5
5
|
Project-URL: Homepage, https://gitstow.com
|
|
6
6
|
Project-URL: Repository, https://github.com/rishmadaan/gitstow
|
|
@@ -71,8 +71,10 @@ pipx install gitstow # recommended — full install: CLI + browser dashboard
|
|
|
71
71
|
# sudo apt install pipx (Debian/Ubuntu)
|
|
72
72
|
# brew install pipx (macOS)
|
|
73
73
|
|
|
74
|
-
# First-run setup
|
|
74
|
+
# First-run setup — creates your first workspace, sets your default host
|
|
75
75
|
gitstow onboard
|
|
76
|
+
# ...or skip the wizard and add a workspace directly:
|
|
77
|
+
# gitstow workspace add ~/oss --label oss
|
|
76
78
|
|
|
77
79
|
# Add repos (GitHub shorthand or full URLs)
|
|
78
80
|
gitstow add anthropic/claude-code
|
|
@@ -309,7 +311,9 @@ active:
|
|
|
309
311
|
2. **Folder-as-state** — The directory structure is the primary source of truth. `repos.yaml` supplements with metadata (frozen, tags, timestamps).
|
|
310
312
|
3. **Error isolation** — One bad repo never stops operations on others. Failures are collected and reported in a summary.
|
|
311
313
|
4. **Parallel execution** — Bulk operations use `asyncio` with a semaphore (default 6 concurrent) to prevent SSH connection storms.
|
|
312
|
-
5. **
|
|
314
|
+
5. **Explicit state** — gitstow never invents a workspace. A fresh install has none, and
|
|
315
|
+
commands that need one say exactly how to create it. Zero workspaces is a valid state:
|
|
316
|
+
you can remove your last workspace and gitstow will simply say so.
|
|
313
317
|
|
|
314
318
|
### A note on folder structure
|
|
315
319
|
|
|
@@ -323,6 +327,8 @@ Flat workspaces skip the owner directory entirely — repos are just `workspace/
|
|
|
323
327
|
|
|
324
328
|
**SSH clone fails with "Permission denied (publickey)"** — Your SSH key isn't configured for the git host. Either add your key (`ssh-add`) or use HTTPS: `gitstow config set prefer_ssh false`.
|
|
325
329
|
|
|
330
|
+
**`No workspaces configured`** — Expected on a fresh install, and after removing your last workspace. gitstow never invents one. Run `gitstow workspace add <path> --label <name>` or `gitstow onboard`.
|
|
331
|
+
|
|
326
332
|
**Workspace directory doesn't exist** — Run `gitstow doctor` to check workspace health. Create missing directories or update the path with `gitstow workspace add`.
|
|
327
333
|
|
|
328
334
|
**`gitstow doctor`** — Run this first when something isn't working. It checks git installation, config files, and workspace integrity.
|
|
@@ -26,8 +26,10 @@ pipx install gitstow # recommended — full install: CLI + browser dashboard
|
|
|
26
26
|
# sudo apt install pipx (Debian/Ubuntu)
|
|
27
27
|
# brew install pipx (macOS)
|
|
28
28
|
|
|
29
|
-
# First-run setup
|
|
29
|
+
# First-run setup — creates your first workspace, sets your default host
|
|
30
30
|
gitstow onboard
|
|
31
|
+
# ...or skip the wizard and add a workspace directly:
|
|
32
|
+
# gitstow workspace add ~/oss --label oss
|
|
31
33
|
|
|
32
34
|
# Add repos (GitHub shorthand or full URLs)
|
|
33
35
|
gitstow add anthropic/claude-code
|
|
@@ -264,7 +266,9 @@ active:
|
|
|
264
266
|
2. **Folder-as-state** — The directory structure is the primary source of truth. `repos.yaml` supplements with metadata (frozen, tags, timestamps).
|
|
265
267
|
3. **Error isolation** — One bad repo never stops operations on others. Failures are collected and reported in a summary.
|
|
266
268
|
4. **Parallel execution** — Bulk operations use `asyncio` with a semaphore (default 6 concurrent) to prevent SSH connection storms.
|
|
267
|
-
5. **
|
|
269
|
+
5. **Explicit state** — gitstow never invents a workspace. A fresh install has none, and
|
|
270
|
+
commands that need one say exactly how to create it. Zero workspaces is a valid state:
|
|
271
|
+
you can remove your last workspace and gitstow will simply say so.
|
|
268
272
|
|
|
269
273
|
### A note on folder structure
|
|
270
274
|
|
|
@@ -278,6 +282,8 @@ Flat workspaces skip the owner directory entirely — repos are just `workspace/
|
|
|
278
282
|
|
|
279
283
|
**SSH clone fails with "Permission denied (publickey)"** — Your SSH key isn't configured for the git host. Either add your key (`ssh-add`) or use HTTPS: `gitstow config set prefer_ssh false`.
|
|
280
284
|
|
|
285
|
+
**`No workspaces configured`** — Expected on a fresh install, and after removing your last workspace. gitstow never invents one. Run `gitstow workspace add <path> --label <name>` or `gitstow onboard`.
|
|
286
|
+
|
|
281
287
|
**Workspace directory doesn't exist** — Run `gitstow doctor` to check workspace health. Create missing directories or update the path with `gitstow workspace add`.
|
|
282
288
|
|
|
283
289
|
**`gitstow doctor`** — Run this first when something isn't working. It checks git installation, config files, and workspace integrity.
|
|
@@ -80,7 +80,7 @@ gitstow add anthropic/claude-code --update
|
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
**Behavior:**
|
|
83
|
-
- Repos are added to the
|
|
83
|
+
- Repos are added to the first configured workspace, or the workspace specified with `-w`. With none configured, `add` exits 1 with `No workspaces configured…` and clones nothing
|
|
84
84
|
- If the repo is already tracked: skips (or pulls with `--update`)
|
|
85
85
|
- If the path exists on disk but isn't tracked: registers it automatically
|
|
86
86
|
- If the path exists on disk with a **different** remote than the one requested: errors with a remote-mismatch message instead of silently registering — move the directory or pick another workspace
|
|
@@ -628,7 +628,7 @@ Valid keys: `default_host`, `prefer_ssh`, `parallel_limit`, `clone_timeout` (clo
|
|
|
628
628
|
Move one workspace's repos to a new directory and update that workspace's `path` in config.
|
|
629
629
|
|
|
630
630
|
```bash
|
|
631
|
-
gitstow config migrate-root ~/new-location # Moves the
|
|
631
|
+
gitstow config migrate-root ~/new-location # Moves the first configured workspace
|
|
632
632
|
gitstow -w active config migrate-root ~/new-location # Moves a specific workspace
|
|
633
633
|
gitstow config migrate-root ~/new-location --copy # Keep originals
|
|
634
634
|
gitstow config migrate-root ~/new-location --yes # Skip confirmation
|
|
@@ -685,6 +685,18 @@ gitstow workspace list
|
|
|
685
685
|
gitstow workspace list --quiet # One label per line (for scripting/completions)
|
|
686
686
|
```
|
|
687
687
|
|
|
688
|
+
With no workspaces configured — a fresh install, or after removing your last one —
|
|
689
|
+
the command exits **0** (this is a valid state, not an error) and prints:
|
|
690
|
+
|
|
691
|
+
```
|
|
692
|
+
No workspaces configured. Add one with:
|
|
693
|
+
gitstow workspace add <path> --label <name>
|
|
694
|
+
or run:
|
|
695
|
+
gitstow onboard
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
`--quiet` prints nothing in that case, so scripts see an empty list.
|
|
699
|
+
|
|
688
700
|
### `gitstow workspace add`
|
|
689
701
|
|
|
690
702
|
Add a new workspace.
|
|
@@ -729,7 +741,28 @@ gitstow workspace remove <label> [flags]
|
|
|
729
741
|
|------|-------------|
|
|
730
742
|
| `--keep-repos/--untrack-repos` | Keep tracked repos in the store (default) or untrack them. |
|
|
731
743
|
|
|
732
|
-
|
|
744
|
+
**Removing your last workspace is allowed.** Zero workspaces is a valid state —
|
|
745
|
+
gitstow does not invent one to fill the gap. The command reports the removal and
|
|
746
|
+
then tells you how to get back:
|
|
747
|
+
|
|
748
|
+
```
|
|
749
|
+
✓ Workspace oss removed
|
|
750
|
+
No workspaces configured. Add one with:
|
|
751
|
+
gitstow workspace add <path> --label <name>
|
|
752
|
+
or run:
|
|
753
|
+
gitstow onboard
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
Until you add one, commands that sweep workspaces (`add`, `pull`, `fetch`, `list`, `status`, `exec`, `search`, `stats`, `migrate`,
|
|
757
|
+
`collection import`, `config migrate-root` and `shell pick`)
|
|
758
|
+
exit **1** with that same message on stderr. Run with `--json`, the same failure
|
|
759
|
+
is emitted as `{"success": false, "error": "..."}` on stdout instead (still exit
|
|
760
|
+
1), so a parsed run never sees an empty stdout — `collection import`,
|
|
761
|
+
`config migrate-root` and `shell pick` have no `--json` option and always use the
|
|
762
|
+
prose form. `workspace list`, `config show` and
|
|
763
|
+
`doctor` still run, and each points at the same fix. Commands that name a single
|
|
764
|
+
repo (`diff`, `open`, `remove`, `repo …`) report that the repo isn't tracked, which
|
|
765
|
+
is what actually happened.
|
|
733
766
|
|
|
734
767
|
### `gitstow workspace scan`
|
|
735
768
|
|
|
@@ -53,11 +53,22 @@ gitstow pull --workspace oss
|
|
|
53
53
|
gitstow repo info anthropic/claude-code -w work
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
> **Migration from single root:** If you have a pre-workspace config with `root_path`, gitstow auto-migrates it to a single workspace labeled `oss` on first use. No action needed.
|
|
56
|
+
> **Migration from single root:** If you have a pre-workspace config with `root_path`, gitstow auto-migrates it to a single workspace labeled `oss` on first use, and rewrites `config.yaml` to match. No action needed.
|
|
57
|
+
>
|
|
58
|
+
> This is the only case where gitstow creates a workspace for you, and it persists it. A config with no `root_path` and no workspaces stays empty.
|
|
57
59
|
|
|
58
60
|
## Folder Structure
|
|
59
61
|
|
|
60
|
-
When you add a repo, gitstow places it in
|
|
62
|
+
When you add a repo, gitstow places it in your first configured workspace (or the one you
|
|
63
|
+
specify with `-w`). A fresh install has none — gitstow does not invent one, so create a
|
|
64
|
+
workspace before adding repos. Every command that sweeps workspaces says so:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Error: No workspaces configured. Add one with:
|
|
68
|
+
gitstow workspace add <path> --label <name>
|
|
69
|
+
or run:
|
|
70
|
+
gitstow onboard
|
|
71
|
+
```
|
|
61
72
|
|
|
62
73
|
### Structured layout (default)
|
|
63
74
|
|
|
@@ -36,10 +36,13 @@ gitstow onboard
|
|
|
36
36
|
|
|
37
37
|
| Key | Default | Description |
|
|
38
38
|
|-----|---------|-------------|
|
|
39
|
-
| `workspaces` |
|
|
39
|
+
| `workspaces` | *(none)* | List of workspace directories. Each has a path, label, layout, and optional auto-tags. A fresh install has none — add one with `gitstow workspace add <path> --label <name>` or `gitstow onboard`. An empty list is valid; gitstow never invents a workspace. |
|
|
40
40
|
| `default_host` | `github.com` | Assumed host when you type shorthand like `owner/repo`. |
|
|
41
41
|
| `prefer_ssh` | `false` | If `true`, clones via SSH (`git@host:owner/repo.git`) instead of HTTPS. |
|
|
42
42
|
| `parallel_limit` | `6` | Maximum concurrent git operations during `pull` and `status`. |
|
|
43
|
+
| `clone_timeout` | `300` | Seconds before a clone is abandoned. Raise it for very large repos. |
|
|
44
|
+
| `ui_tailscale` | `false` | If `true`, `gitstow ui` also serves the dashboard on this machine's Tailscale address. See [Web Dashboard over Tailscale](#web-dashboard-over-tailscale). |
|
|
45
|
+
| `config_version` | *(written by gitstow)* | Internal — records which version of gitstow last wrote this file, so one-time upgrade migrations know they have already run. Do not edit. |
|
|
43
46
|
|
|
44
47
|
## File Locations
|
|
45
48
|
|
|
@@ -61,6 +64,9 @@ workspaces:
|
|
|
61
64
|
default_host: github.com
|
|
62
65
|
prefer_ssh: false
|
|
63
66
|
parallel_limit: 6
|
|
67
|
+
clone_timeout: 300
|
|
68
|
+
ui_tailscale: false
|
|
69
|
+
config_version: 2
|
|
64
70
|
```
|
|
65
71
|
|
|
66
72
|
Each workspace entry has:
|
|
@@ -163,6 +169,32 @@ If you see SSH connection errors during bulk pulls, lower it:
|
|
|
163
169
|
gitstow config set parallel_limit 3
|
|
164
170
|
```
|
|
165
171
|
|
|
172
|
+
## Web Dashboard over Tailscale
|
|
173
|
+
|
|
174
|
+
By default the dashboard (`gitstow ui`) binds `127.0.0.1` only. On a VPS or
|
|
175
|
+
home server you can also serve it on the machine's own Tailscale address, so
|
|
176
|
+
any device on your tailnet can open it:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
gitstow config set ui_tailscale true # every gitstow ui from now on
|
|
180
|
+
gitstow ui --tailscale # or per-run (--no-tailscale to skip once)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The same setting can be flipped from the dashboard's own **Settings** page,
|
|
184
|
+
and `gitstow onboard` offers it during setup when Tailscale is installed.
|
|
185
|
+
|
|
186
|
+
Things worth knowing:
|
|
187
|
+
|
|
188
|
+
- The setting applies when the server starts (that's when the socket binds).
|
|
189
|
+
Toggling it in the dashboard saves the config and tells you to restart
|
|
190
|
+
`gitstow ui` — it never pretends to change the running server.
|
|
191
|
+
- It never binds `0.0.0.0`. The trust boundary is your tailnet: traffic is
|
|
192
|
+
WireGuard-encrypted, and anyone on your tailnet can reach the dashboard.
|
|
193
|
+
- The startup message prints the tailnet URL as `http://<tailscale-ip>:7853`.
|
|
194
|
+
The MagicDNS name works too if you type it.
|
|
195
|
+
- If Tailscale isn't installed or the daemon isn't running, `gitstow ui`
|
|
196
|
+
warns and serves localhost only — it never fails to start over this.
|
|
197
|
+
|
|
166
198
|
## MCP Server (Optional)
|
|
167
199
|
|
|
168
200
|
> **Most users don't need this.** The Claude Code skill is the recommended AI integration — it has zero context overhead and auto-updates on version bumps. The MCP server is only for AI tools that don't support Claude Code skills (Claude Desktop, Cursor, Windsurf).
|
|
@@ -41,7 +41,34 @@ gitstow --version
|
|
|
41
41
|
|
|
42
42
|
> **Command not found?** On some systems, pip installs to a directory not on your PATH. Try `python3 -m gitstow --version` instead, or use pipx which handles PATH automatically.
|
|
43
43
|
|
|
44
|
-
## 2.
|
|
44
|
+
## 2. Create a Workspace
|
|
45
|
+
|
|
46
|
+
A fresh install has **no workspaces**. A workspace is the directory gitstow clones
|
|
47
|
+
into, and gitstow never invents one for you — so this is the first step.
|
|
48
|
+
|
|
49
|
+
The interactive wizard walks you through it (and suggests `~/opensource` as a path):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
gitstow onboard
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or do it in one line:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
gitstow workspace add ~/oss --label oss --layout structured
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Until a workspace exists, commands that need one (`add`, `pull`, `list`, `status`, …)
|
|
62
|
+
stop with:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Error: No workspaces configured. Add one with:
|
|
66
|
+
gitstow workspace add <path> --label <name>
|
|
67
|
+
or run:
|
|
68
|
+
gitstow onboard
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 3. Add Your First Repo
|
|
45
72
|
|
|
46
73
|
```bash
|
|
47
74
|
gitstow add anthropic/claude-code
|
|
@@ -49,12 +76,13 @@ gitstow add anthropic/claude-code
|
|
|
49
76
|
|
|
50
77
|
That's it. gitstow:
|
|
51
78
|
1. Recognizes `anthropic/claude-code` as GitHub shorthand
|
|
52
|
-
2. Clones to `~/oss/anthropic/claude-code/` (your
|
|
79
|
+
2. Clones to `~/oss/anthropic/claude-code/` (your first configured workspace)
|
|
53
80
|
3. Registers it in your collection
|
|
54
81
|
|
|
55
|
-
> **
|
|
82
|
+
> **Which workspace?** With more than one configured, `gitstow add` uses the first
|
|
83
|
+
> one listed in `config.yaml`. Target another with `-w`, e.g. `gitstow -w active add owner/repo`.
|
|
56
84
|
|
|
57
|
-
##
|
|
85
|
+
## 4. Add More Repos
|
|
58
86
|
|
|
59
87
|
```bash
|
|
60
88
|
# GitHub shorthand (most common)
|
|
@@ -70,7 +98,7 @@ gitstow add git@bitbucket.org:owner/repo.git
|
|
|
70
98
|
gitstow add torvalds/linux --shallow
|
|
71
99
|
```
|
|
72
100
|
|
|
73
|
-
##
|
|
101
|
+
## 5. See Your Collection
|
|
74
102
|
|
|
75
103
|
```bash
|
|
76
104
|
gitstow list
|
|
@@ -90,7 +118,7 @@ Output:
|
|
|
90
118
|
linux just now
|
|
91
119
|
```
|
|
92
120
|
|
|
93
|
-
##
|
|
121
|
+
## 6. Update Everything
|
|
94
122
|
|
|
95
123
|
```bash
|
|
96
124
|
gitstow pull
|
|
@@ -110,7 +138,7 @@ Output:
|
|
|
110
138
|
|
|
111
139
|
Every repo is pulled in parallel (up to 6 at once). If one repo fails, the others still update — you get a summary at the end.
|
|
112
140
|
|
|
113
|
-
##
|
|
141
|
+
## 7. Check Status
|
|
114
142
|
|
|
115
143
|
```bash
|
|
116
144
|
gitstow status
|
|
@@ -132,7 +160,7 @@ For a guided first-run experience:
|
|
|
132
160
|
gitstow onboard
|
|
133
161
|
```
|
|
134
162
|
|
|
135
|
-
This walks you through
|
|
163
|
+
This walks you through creating your first workspace (suggesting `~/opensource` as its path), choosing a default git host and SSH vs HTTPS preference, then scans for existing repos to register.
|
|
136
164
|
|
|
137
165
|
## Multiple Workspaces
|
|
138
166
|
|
|
@@ -170,7 +198,7 @@ This is also done automatically during `gitstow onboard` and auto-updates when y
|
|
|
170
198
|
gitstow ui
|
|
171
199
|
```
|
|
172
200
|
|
|
173
|
-
Opens a local dark-themed dashboard at `http://127.0.0.1:7853` — a tab you leave open. Shows dirty state across every repo at a glance, pulls single repos or all-of-them in parallel, adds/removes repos, edits tags. Auto-refreshes every 30 seconds. Click **Shutdown** in the footer (or Ctrl+C) when done. See [commands.md — `gitstow ui`](commands.md#gitstow-ui) for full details.
|
|
201
|
+
Opens a local dark-themed dashboard at `http://127.0.0.1:7853` — a tab you leave open. Shows dirty state across every repo at a glance, pulls single repos or all-of-them in parallel, adds/removes repos, edits tags. Auto-refreshes every 30 seconds. Click **Shutdown** in the footer (or Ctrl+C) when done. Running gitstow on a VPS? `gitstow ui --tailscale` (or the Tailscale toggle in the dashboard's Settings) also serves it on your tailnet so you can open it from your other devices — see [configuration.md](configuration.md#web-dashboard-over-tailscale). See [commands.md — `gitstow ui`](commands.md#gitstow-ui) for full details.
|
|
174
202
|
|
|
175
203
|
## Keep gitstow Up to Date
|
|
176
204
|
|
|
@@ -195,6 +223,10 @@ gitstow update # upgrade to the latest PyPI version
|
|
|
195
223
|
- For SSH: check your SSH key is added (`ssh -T git@github.com`)
|
|
196
224
|
- Set SSH as default: `gitstow config set prefer_ssh true`
|
|
197
225
|
|
|
226
|
+
**"No workspaces configured"**
|
|
227
|
+
- Expected on a fresh install, and after removing your last workspace — zero workspaces is a valid state, not a broken one.
|
|
228
|
+
- Fix it with `gitstow workspace add <path> --label <name>` or the `gitstow onboard` wizard. The same message appears in the web dashboard, with a link to the workspaces page.
|
|
229
|
+
|
|
198
230
|
**"gitstow doctor" for diagnostics**
|
|
199
231
|
```bash
|
|
200
232
|
gitstow doctor
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitstow"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.2"
|
|
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"
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
</div>
|
|
74
74
|
<p class="pipx-note" id="tab-note">Already have pipx? <code class="code-chip">pipx install gitstow</code> alone does it.</p>
|
|
75
75
|
<div class="term-lines">
|
|
76
|
-
<div><span class="prompt">$</span> <span class="cmd">gitstow onboard</span> <span class="comment">#
|
|
76
|
+
<div><span class="prompt">$</span> <span class="cmd">gitstow onboard</span> <span class="comment"># first-run wizard: creates your workspace, installs the Claude skill</span></div>
|
|
77
77
|
<div><span class="prompt">$</span> <span class="cmd">gitstow add anthropic/claude-code</span> <span class="comment"># shorthand or full URLs</span></div>
|
|
78
78
|
<div><span class="prompt">$</span> <span class="cmd">gitstow pull</span> <span class="comment"># update everything, in parallel</span></div>
|
|
79
79
|
<div><span class="prompt">$</span> <span class="cmd">gitstow ui</span> <span class="comment"># open the dashboard ↓</span></div>
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
<div class="center-head">
|
|
98
98
|
<div class="eyebrow"><span class="bar"></span>gitstow ui<span class="bar"></span></div>
|
|
99
99
|
<h2 class="section">The tab you leave open.</h2>
|
|
100
|
-
<p>A local-first browser dashboard, and the primary way to live with your library. Dirty state at a glance, one-click pulls, tags, freezes, and per-file diffs. It auto-refreshes every 30 seconds and never phones home: <code class="code-chip">127.0.0.1</code> by default — or flip on Tailscale access and open it from any device on your tailnet. Never the public internet.</p>
|
|
100
|
+
<p>A local-first browser dashboard, and the primary way to live with your library. Dirty state at a glance, one-click pulls, tags, freezes, and per-file diffs. It auto-refreshes every 30 seconds and never phones home: <code class="code-chip">127.0.0.1</code> by default — or flip on Tailscale access — right from its Settings page — and open it from any device on your tailnet. Never the public internet.</p>
|
|
101
101
|
</div>
|
|
102
102
|
<div class="bw-scroll">
|
|
103
103
|
<div class="bw">
|
|
@@ -67,7 +67,8 @@ def add(
|
|
|
67
67
|
"""[bold green]Add[/bold green] repos — clone into organized structure.
|
|
68
68
|
|
|
69
69
|
Accepts full URLs, SSH URLs, or shorthand (owner/repo assumes GitHub).
|
|
70
|
-
|
|
70
|
+
Clones into the first configured workspace unless -w is specified;
|
|
71
|
+
errors with a hint if none is configured.
|
|
71
72
|
|
|
72
73
|
\b
|
|
73
74
|
Examples:
|
|
@@ -79,8 +80,8 @@ def add(
|
|
|
79
80
|
settings = load_config()
|
|
80
81
|
store = RepoStore()
|
|
81
82
|
ws_label = ctx.obj.get("workspace") if ctx.obj else None
|
|
82
|
-
ws_list = resolve_workspaces(settings, ws_label)
|
|
83
|
-
ws = ws_list[0] #
|
|
83
|
+
ws_list = resolve_workspaces(settings, ws_label, output_json=output_json)
|
|
84
|
+
ws = ws_list[0] # The -w workspace, or the first configured one
|
|
84
85
|
root = ws.get_path()
|
|
85
86
|
tags = list(tag or []) + list(ws.auto_tags)
|
|
86
87
|
|
|
@@ -8,6 +8,7 @@ import sys
|
|
|
8
8
|
import typer
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
|
|
11
|
+
from gitstow.cli.helpers import print_no_workspaces_hint, resolve_workspaces
|
|
11
12
|
from gitstow.core.config import load_config, save_config
|
|
12
13
|
from gitstow.core.paths import CONFIG_FILE, REPOS_FILE
|
|
13
14
|
from gitstow.core.repo import RepoStore
|
|
@@ -57,6 +58,8 @@ def config_show(
|
|
|
57
58
|
# Show workspaces
|
|
58
59
|
workspaces = settings.get_workspaces()
|
|
59
60
|
console.print(f" [bold]Workspaces ({len(workspaces)}):[/bold]")
|
|
61
|
+
if not workspaces:
|
|
62
|
+
print_no_workspaces_hint(console, indent=" ")
|
|
60
63
|
ws_counts = store.all_workspaces()
|
|
61
64
|
for ws in workspaces:
|
|
62
65
|
count = ws_counts.get(ws.label, 0)
|
|
@@ -92,6 +95,9 @@ def config_set(
|
|
|
92
95
|
"""
|
|
93
96
|
settings = load_config()
|
|
94
97
|
|
|
98
|
+
# config_version is deliberately absent: it is provenance gitstow writes, not
|
|
99
|
+
# a setting. Editing it would either re-trigger or permanently disable the
|
|
100
|
+
# one-time legacy migrations.
|
|
95
101
|
valid_keys = {"default_host", "prefer_ssh", "parallel_limit", "clone_timeout", "ui_tailscale"}
|
|
96
102
|
if key not in valid_keys:
|
|
97
103
|
err_console.print(
|
|
@@ -138,7 +144,7 @@ def config_migrate_root(
|
|
|
138
144
|
|
|
139
145
|
\b
|
|
140
146
|
Examples:
|
|
141
|
-
gitstow config migrate-root ~/new-location #
|
|
147
|
+
gitstow config migrate-root ~/new-location # first configured workspace
|
|
142
148
|
gitstow -w active config migrate-root ~/new-location # specific workspace
|
|
143
149
|
"""
|
|
144
150
|
import shutil
|
|
@@ -149,16 +155,9 @@ def config_migrate_root(
|
|
|
149
155
|
settings = load_config()
|
|
150
156
|
store = RepoStore()
|
|
151
157
|
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
ws_label = (ctx.obj or {}).get("workspace") or settings.get_default_workspace().label
|
|
157
|
-
ws = settings.get_workspace(ws_label)
|
|
158
|
-
if ws is None:
|
|
159
|
-
labels = ", ".join(w.label for w in settings.get_workspaces())
|
|
160
|
-
err_console.print(f"[red]Error:[/red] Unknown workspace [bold]{ws_label}[/bold]. Available: {labels}")
|
|
161
|
-
raise typer.Exit(code=1)
|
|
158
|
+
# No configured workspace → the shared hint + exit 1; unknown -w label → the
|
|
159
|
+
# shared "Unknown workspace" error; otherwise the named or first workspace.
|
|
160
|
+
ws = resolve_workspaces(settings, (ctx.obj or {}).get("workspace"))[0]
|
|
162
161
|
|
|
163
162
|
old_root = ws.get_path()
|
|
164
163
|
new_root_path = Path(new_root).expanduser().resolve()
|
|
@@ -7,8 +7,10 @@ import sys
|
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
from rich.console import Console
|
|
10
|
+
from rich.markup import escape
|
|
10
11
|
|
|
11
12
|
from gitstow import __version__
|
|
13
|
+
from gitstow.cli.helpers import print_no_workspaces_hint
|
|
12
14
|
from gitstow.core.config import load_config
|
|
13
15
|
from gitstow.core.discovery import discover_repos, reconcile
|
|
14
16
|
from gitstow.core.git import is_git_installed
|
|
@@ -44,6 +46,9 @@ def doctor(
|
|
|
44
46
|
"repos_file_exists": repos_file.exists(),
|
|
45
47
|
"repos_file_path": str(repos_file),
|
|
46
48
|
"workspaces": len(workspaces),
|
|
49
|
+
# A config with zero workspaces is valid but unusable — surface it as a
|
|
50
|
+
# failed check rather than a silent pass.
|
|
51
|
+
"workspaces_configured": bool(workspaces),
|
|
47
52
|
"repos_tracked": store.count(),
|
|
48
53
|
}
|
|
49
54
|
|
|
@@ -104,11 +109,16 @@ def doctor(
|
|
|
104
109
|
_check("App directory", APP_HOME.exists())
|
|
105
110
|
_check("Config file", CONFIG_FILE.exists())
|
|
106
111
|
_check("Repos file", repos_file.exists(), str(repos_file))
|
|
107
|
-
|
|
112
|
+
_check(
|
|
113
|
+
"Workspaces", bool(workspaces),
|
|
114
|
+
f"{len(workspaces)} configured" if workspaces else "",
|
|
115
|
+
fail_label="None configured",
|
|
116
|
+
)
|
|
108
117
|
console.print(f" Repos tracked: {store.count()}")
|
|
109
118
|
|
|
110
|
-
if not
|
|
111
|
-
console.print(
|
|
119
|
+
if not workspaces:
|
|
120
|
+
console.print()
|
|
121
|
+
print_no_workspaces_hint(console, style="yellow", indent=" ")
|
|
112
122
|
|
|
113
123
|
# Per-workspace health
|
|
114
124
|
console.print("\n [bold]3. Workspace Health[/bold]\n")
|
|
@@ -125,28 +135,36 @@ def doctor(
|
|
|
125
135
|
if total_orphaned:
|
|
126
136
|
console.print(f"\n [yellow]⚠ {len(total_orphaned)} untracked repos on disk:[/yellow]")
|
|
127
137
|
for ws_name, key in total_orphaned:
|
|
128
|
-
|
|
138
|
+
# escape() the whole bracketed label: Rich reads a bare [label] as a
|
|
139
|
+
# style tag and swallows it, printing a label-less row.
|
|
140
|
+
console.print(f" {escape(f'[{ws_name}]')} {key}")
|
|
129
141
|
|
|
130
142
|
if total_missing:
|
|
131
143
|
console.print(f"\n [yellow]⚠ {len(total_missing)} tracked but missing from disk:[/yellow]")
|
|
132
144
|
for ws_name, key in total_missing:
|
|
133
|
-
console.print(f" [{ws_name}] {key}")
|
|
145
|
+
console.print(f" {escape(f'[{ws_name}]')} {key}")
|
|
134
146
|
|
|
135
|
-
if not
|
|
147
|
+
if not workspaces:
|
|
148
|
+
console.print(" [dim]Nothing to check — no workspaces configured.[/dim]")
|
|
149
|
+
elif not total_orphaned and not total_missing:
|
|
136
150
|
console.print(f"\n [green]✓ All repos in sync across {len(workspaces)} workspace(s)[/green]")
|
|
137
151
|
|
|
138
152
|
if orphaned_ws:
|
|
139
153
|
console.print("\n [yellow]⚠ Repos tracked under removed workspaces (invisible to list/status):[/yellow]")
|
|
140
154
|
for label, count in orphaned_ws.items():
|
|
141
|
-
console.print(
|
|
155
|
+
console.print(
|
|
156
|
+
f" {escape(f'[{label}]')} {count} repo{'s' if count != 1 else ''}"
|
|
157
|
+
)
|
|
142
158
|
console.print(
|
|
143
159
|
" [dim]Clear with 'gitstow workspace remove <label>', or re-add the workspace "
|
|
144
160
|
"with 'gitstow workspace add <path> --label <label>'.[/dim]"
|
|
145
161
|
)
|
|
146
162
|
|
|
147
163
|
# 4. SSH connectivity hint
|
|
164
|
+
# Skipped entirely with nothing configured: an up-to-10s network probe to
|
|
165
|
+
# help repos that no workspace can reach is time spent for no answer.
|
|
148
166
|
ssh_repos = [r for r in store.list_all() if r.remote_url and r.remote_url.startswith("git@")]
|
|
149
|
-
if ssh_repos:
|
|
167
|
+
if workspaces and ssh_repos:
|
|
150
168
|
console.print("\n [bold]4. SSH Connectivity[/bold]\n")
|
|
151
169
|
ssh_ok = _check_ssh_connectivity()
|
|
152
170
|
if ssh_ok:
|
|
@@ -173,8 +191,8 @@ def _check_ssh_connectivity() -> bool:
|
|
|
173
191
|
return False
|
|
174
192
|
|
|
175
193
|
|
|
176
|
-
def _check(label: str, ok: bool, detail: str = "") -> None:
|
|
194
|
+
def _check(label: str, ok: bool, detail: str = "", fail_label: str = "Missing") -> None:
|
|
177
195
|
"""Print a check result."""
|
|
178
|
-
status = "[green]OK[/green]" if ok else "[red]
|
|
196
|
+
status = "[green]OK[/green]" if ok else f"[red]{fail_label}[/red]"
|
|
179
197
|
detail_str = f" [dim]({detail})[/dim]" if detail else ""
|
|
180
198
|
console.print(f" {label}: {status}{detail_str}")
|