odoo-workspaces 2.1.0__tar.gz → 2.2.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.
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/AGENTS.md +11 -1
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/PKG-INFO +1 -1
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/docs/commands.md +79 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/odoo_workspaces.egg-info/PKG-INFO +1 -1
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/odoo_workspaces.egg-info/SOURCES.txt +11 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/odoo_workspaces.egg-info/scm_file_list.json +11 -0
- odoo_workspaces-2.2.0/src/odoo_workspaces.egg-info/scm_version.json +8 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/__main__.py +30 -2
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_version.py +3 -3
- odoo_workspaces-2.2.0/src/ow/askpass.py +41 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/__init__.py +4 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/prune.py +83 -7
- odoo_workspaces-2.2.0/src/ow/commands/pull.py +230 -0
- odoo_workspaces-2.2.0/src/ow/commands/reset.py +274 -0
- odoo_workspaces-2.2.0/src/ow/utils/askpass.py +180 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/git.py +30 -1
- odoo_workspaces-2.2.0/src/ow/utils/pull_plan.py +104 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/refs.py +22 -1
- odoo_workspaces-2.2.0/src/ow/utils/reset_plan.py +94 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_prune.py +107 -0
- odoo_workspaces-2.2.0/src/tests/commands/test_pull.py +294 -0
- odoo_workspaces-2.2.0/src/tests/commands/test_reset.py +301 -0
- odoo_workspaces-2.2.0/src/tests/utils/test_askpass.py +129 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_git_primitives.py +20 -0
- odoo_workspaces-2.2.0/src/tests/utils/test_pull_plan.py +135 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_refs.py +22 -0
- odoo_workspaces-2.2.0/src/tests/utils/test_reset_plan.py +127 -0
- odoo_workspaces-2.1.0/src/odoo_workspaces.egg-info/scm_version.json +0 -8
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/.github/workflows/release.yml +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/.gitignore +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/.vscode/launch.json +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/.vscode/settings.json +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/LICENSE +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/README.md +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/RELEASE.md +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/docs/configuration.md +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/docs/migrating-to-2.0.md +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/docs/sandboxing.md +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/docs/services.md +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/docs/templates.md +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/mise.toml +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/pyproject.toml +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/scripts/bwrap-claude +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/scripts/bwrap-opencode +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/scripts/migrate-to-2.0.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/scripts/tests/__init__.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/scripts/tests/test_migrate_2.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/setup.cfg +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/odoo_workspaces.egg-info/dependency_links.txt +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/odoo_workspaces.egg-info/entry_points.txt +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/odoo_workspaces.egg-info/requires.txt +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/odoo_workspaces.egg-info/top_level.txt +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/__init__.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/services/compose.yml.j2 +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/templates/bwrap/bwrap-claude.j2 +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/templates/bwrap/bwrap-opencode.j2 +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/templates/common/mise.toml.j2 +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/templates/common/odools.toml.j2 +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/templates/common/odoorc.j2 +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/templates/common/pyrightconfig.json.j2 +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/templates/common/requirements-dev.txt +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/templates/vscode/.vscode/launch.json.j2 +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/templates/vscode/.vscode/settings.json.j2 +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/templates/zed/.zed/debug.json.j2 +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/_static/templates/zed/.zed/settings.json.j2 +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/apply.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/archive.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/cd.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/init.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/ls.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/mv.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/open.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/rebase.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/rm.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/shell_init.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/status.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/commands/templates.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/config.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/display.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/drift.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/index.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/legacy.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/paths.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/rebase_plan.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/relocate.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/resolver.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/ow/utils/templates.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/__init__.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/__init__.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_apply.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_archive.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_init.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_init_extended.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_ls.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_misc_extended.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_mv.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_open.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_rebase.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_rebase_scenarios.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_rm.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_shell_init.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_status.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_status_extended.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_status_extended2.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/commands/test_templates_cmd.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/conftest.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/test_cli.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/test_main_extended.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/__init__.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_config.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_display.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_drift.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_git.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_index.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_interrupt.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_legacy.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_misc.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_paths.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_progress.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_rebase_plan.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_resolve_template_files.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_resolver.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_templates.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_templates_extended.py +0 -0
- {odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/tests/utils/test_templates_extended2.py +0 -0
|
@@ -12,6 +12,8 @@ src/
|
|
|
12
12
|
│ │ ├── apply.py # cmd_apply — make the tree match .ow/config.toml (--only narrows the git work, never the rendering)
|
|
13
13
|
│ │ ├── status.py # cmd_status + display helpers
|
|
14
14
|
│ │ ├── rebase.py # cmd_rebase + fact gathering, display, execution
|
|
15
|
+
│ │ ├── pull.py # cmd_pull + fact gathering, display, execution (fast-forward only)
|
|
16
|
+
│ │ ├── reset.py # cmd_reset + fact gathering, display, execution (git reset, per repo)
|
|
15
17
|
│ │ ├── prune.py # cmd_prune
|
|
16
18
|
│ │ ├── rm.py # cmd_rm — remove a workspace: worktrees, local branches, directory, index entry
|
|
17
19
|
│ │ └── templates.py # cmd_templates — list/take/diff template files; outdated_templates() used by cmd_apply
|
|
@@ -25,6 +27,9 @@ src/
|
|
|
25
27
|
│ │ ├── paths.py # ow's locations, resolved from the XDG base directories
|
|
26
28
|
│ │ ├── refs.py # fetch_workspace_refs
|
|
27
29
|
│ │ ├── rebase_plan.py # RepoFacts, GitStep, RebasePlan, plan_for (pure)
|
|
30
|
+
│ │ ├── pull_plan.py # PullFacts, PullPlan, plan_pull (pure)
|
|
31
|
+
│ │ ├── reset_plan.py # ResetFacts, ResetPlan, plan_reset (pure)
|
|
32
|
+
│ │ ├── askpass.py # SSH_ASKPASS broker: git children ask ow, ow asks the terminal
|
|
28
33
|
│ │ ├── resolver.py # resolve_workspace
|
|
29
34
|
│ │ └── templates.py # file generators, template resolution, application, materialization
|
|
30
35
|
│ └── _static/
|
|
@@ -66,9 +71,12 @@ AGENTS.md
|
|
|
66
71
|
- `index.py` — remembers where workspaces live (for name-based resolution and duplicate-branch checks), without owning the truth — the workspace's own `.ow/config.toml` is that.
|
|
67
72
|
- `display.py` — `console`, `err_console`, `counts`, `print_git_result`.
|
|
68
73
|
- `drift.py` — `DriftResult`, `check_drift`, `warn_if_drifted`. Detect when worktree branch state doesn't match config. Commands call `warn_if_drifted` to display warnings but proceed anyway.
|
|
69
|
-
- `git.py` — worktree operations: `create_worktree`, `attach_worktree`, `detach_worktree`, `worktree_exists`, `worktree_is_detached`, `get_worktree_branch`, `get_worktree_head`. Bare repo operations: `ensure_bare_repo`, `ensure_ref`. Ref/branch queries: `resolve_spec`, `resolve_spec_local`, `get_all_remote_refs`, `get_remote_ref_for_branch`, `get_remote_url`, `get_upstream`, `set_branch_upstream`, `is_branch_pushed`. Analysis: `rev_parse`, `is_ancestor`, `merge_base`, `count_commits`, `count_new_patches`, `count_unpushed`, `in_progress_operation`, `dirty_files`. Execution: `git`, `git_fetch`, `parallel_per_repo`, `run_cmd`.
|
|
74
|
+
- `git.py` — worktree operations: `create_worktree`, `attach_worktree`, `detach_worktree`, `worktree_exists`, `worktree_is_detached`, `get_worktree_branch`, `get_worktree_head`. Bare repo operations: `ensure_bare_repo`, `ensure_ref`. Ref/branch queries: `resolve_spec`, `resolve_spec_local`, `get_all_remote_refs`, `get_remote_ref_for_branch`, `get_remote_url`, `get_upstream`, `set_branch_upstream`, `is_branch_pushed`. Analysis: `rev_parse`, `is_ancestor`, `merge_base`, `count_commits`, `count_new_patches`, `count_unpushed`, `count_unbacked_commits`, `in_progress_operation`, `dirty_files`. Execution: `git`, `git_fetch`, `parallel_per_repo`, `run_cmd`.
|
|
70
75
|
- `refs.py` — `fetch_workspace_refs` — three-phase pipeline for fetching workspace refs.
|
|
71
76
|
- `rebase_plan.py` — `RepoFacts`, `GitStep`, `RebasePlan`, `plan_for` — pure analysis functions for rebase planning.
|
|
77
|
+
- `pull_plan.py` — `PullFacts`, `PullPlan`, `plan_pull` — pure analysis functions for fast-forward planning.
|
|
78
|
+
- `reset_plan.py` — `ResetFacts`, `ResetPlan`, `plan_reset` — pure analysis functions for reset planning.
|
|
79
|
+
- `askpass.py` — `broker()`, `child_env()`. Every git child runs in its own session and so has no controlling terminal; ssh therefore falls back to `SSH_ASKPASS`. The broker points it at a generated shim that forwards the prompt over a Unix socket to ow, which still owns the terminal. Prompts are serialised and answers cached per run. Entered once, in `__main__.main()`.
|
|
72
80
|
- `resolver.py` — `resolve_workspace(name=None)` resolves a workspace: an explicit path, an explicit name (looked up via the index), the `OW_WORKSPACE` env var, or a cwd walk-up for `.ow/config.toml`. One rule, four branches, no fallback between them.
|
|
73
81
|
- `templates.py` — `is_odoo_main_repo`, `find_addon_paths`, `build_template_context`, `apply_templates`, `ensure_workspace_materialized`, `available_templates`, `packaged_files`, `resolve_template_files` (local overrides win per file, packaged sibling still reachable via Jinja include/extends).
|
|
74
82
|
|
|
@@ -80,6 +88,8 @@ AGENTS.md
|
|
|
80
88
|
| `ow apply` | `cmd_apply(config, workspace=None, *, only=None)` | Materialize worktrees + re-render templates |
|
|
81
89
|
| `ow status` | `cmd_status(config, workspace=None)` | Show workspace branch status |
|
|
82
90
|
| `ow rebase` | `cmd_rebase(config, workspace=None, *, only=None, autostash=False, dry_run=False, yes=False)` | Fetch + rebase workspace branches |
|
|
91
|
+
| `ow pull` | `cmd_pull(config, workspace=None, *, only=None, dry_run=False)` | Fetch, then fast-forward each repo — or replay it on its own upstream, `git pull --rebase` style. Never moves a repo off its base ref; that stays `ow rebase` |
|
|
92
|
+
| `ow reset` | `cmd_reset(config, workspace=None, *, only=None, hard=False, fetch=False, dry_run=False, yes=False)` | Put each repo back on the ref it follows — its branch's upstream, or the base ref when there is none. Plain form moves HEAD and leaves the working tree, so nothing on disk is lost; `--hard` discards it too. No fetch unless `-f`; skips a repo that is not on the branch the config names |
|
|
83
93
|
| `ow prune` | `cmd_prune(config)` | Clean up stale worktree references, orphaned branches, dead index entries |
|
|
84
94
|
| `ow rm` | `cmd_rm(name, *, yes=False)` | Remove a workspace: worktrees, local branches, directory, index entry |
|
|
85
95
|
| `ow templates` | `cmd_templates(take=None, show_diff=False)` | List template files with their state, take one, or diff the stale ones |
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
| `ow apply` | `[workspace]` | Re-render templates and materialize worktrees |
|
|
9
9
|
| `ow status` | `[workspace]`, `-f/--fetch` | Show branch status with behind/ahead counts |
|
|
10
10
|
| `ow rebase` | `[workspace]`, `--only`, `--autostash`, `--dry-run`, `-y/--yes` | Fetch and rebase repos in a workspace |
|
|
11
|
+
| `ow pull` | `[workspace]`, `--only`, `--dry-run` | Fetch and fast-forward repos in a workspace |
|
|
12
|
+
| `ow reset` | `[workspace]`, `--only`, `--hard`, `-f/--fetch`, `--dry-run`, `-y/--yes` | Put repos back on the refs they follow |
|
|
11
13
|
| `ow prune` | `--dry-run`, `-y/--yes` | Clean up stale worktree references, orphaned branches, and dead index entries |
|
|
12
14
|
| `ow rm` | `<name>`, `-y/--yes` | Remove a workspace: worktrees, local branches, directory, and index entry |
|
|
13
15
|
| `ow ls` | — | List every known workspace, its path, and its repos |
|
|
@@ -111,6 +113,83 @@ ever pushed: the `git push --force-with-lease` stays yours.
|
|
|
111
113
|
`--dry-run` fetches refs to show you what would happen, but runs no command that
|
|
112
114
|
touches your worktrees.
|
|
113
115
|
|
|
116
|
+
## `ow pull`
|
|
117
|
+
|
|
118
|
+
Brings every repo of a workspace up to date without moving any of them off the
|
|
119
|
+
base branch they are configured on. `ow rebase` is the command that does move
|
|
120
|
+
them.
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
ow pull # every repo of the current workspace
|
|
124
|
+
ow pull parrot --only community # one repo of a named workspace
|
|
125
|
+
ow pull --dry-run # fetch, then print the plan — no worktree touched
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Each repo follows its upstream when it has one — its own branch as pushed
|
|
129
|
+
elsewhere — and its base ref otherwise.
|
|
130
|
+
|
|
131
|
+
- already there: left alone
|
|
132
|
+
- detached: re-detaches onto the fetched ref
|
|
133
|
+
- behind: `git merge --ff-only`, which git refuses if the move would clobber a
|
|
134
|
+
local modification
|
|
135
|
+
- diverged **from its upstream**: `git rebase <upstream>`, exactly what
|
|
136
|
+
`git pull --rebase` does — same branch, same base, nobody else's commits
|
|
137
|
+
- diverged **from its base ref**, with no upstream: reported and left alone.
|
|
138
|
+
Carrying local work over to a moved base needs force-push detection and a
|
|
139
|
+
replay floor, and is `ow rebase`'s job
|
|
140
|
+
|
|
141
|
+
A repo is also left alone, and the run exits non-zero, when a git operation is
|
|
142
|
+
already in progress, when a replay would be needed but the worktree is dirty
|
|
143
|
+
(the message names the files), when the worktree is missing, or when the fetch
|
|
144
|
+
failed — moving onto the stale cached ref would look like success.
|
|
145
|
+
|
|
146
|
+
On conflict during a replay, resolve, `git rebase --continue`, then re-run
|
|
147
|
+
`ow pull --only <alias>`. Nothing is ever pushed.
|
|
148
|
+
|
|
149
|
+
## `ow reset`
|
|
150
|
+
|
|
151
|
+
Puts every repo back on the ref it follows — `git reset`, one repo at a time.
|
|
152
|
+
Use it when a workspace has wandered, or when an upstream was force-pushed and
|
|
153
|
+
you want the remote's version of the branch, whatever yours has become.
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
ow reset # every repo of the current workspace
|
|
157
|
+
ow reset parrot --only enterprise # one repo of a named workspace
|
|
158
|
+
ow reset --hard # discard the working tree too
|
|
159
|
+
ow reset --hard -f # ... against freshly fetched refs
|
|
160
|
+
ow reset --dry-run # print the plan, touch nothing
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**The ref each repo is reset to** is the one `git reset @{u}` would have used:
|
|
164
|
+
the branch's upstream — its own copy on a remote — or, for a branch nobody has
|
|
165
|
+
pushed and for a detached repo, the base ref it was cut from. An attached branch
|
|
166
|
+
is never reset onto its base: that would not put the repo back, it would throw
|
|
167
|
+
the whole branch away.
|
|
168
|
+
|
|
169
|
+
The two forms differ exactly as git's do:
|
|
170
|
+
|
|
171
|
+
- `ow reset` moves HEAD and leaves the working tree alone. The commits are gone
|
|
172
|
+
from the branch, but their content is still on disk as unstaged changes —
|
|
173
|
+
**nothing on disk is lost**.
|
|
174
|
+
- `ow reset --hard` discards the working tree as well. Untracked files are left
|
|
175
|
+
alone, exactly as `git reset --hard` leaves them; `git clean` is its own
|
|
176
|
+
command.
|
|
177
|
+
|
|
178
|
+
No fetch by default: it resets to the refs already in the bare repos, the way
|
|
179
|
+
`git reset origin/master` does. `-f/--fetch` refreshes them first, which is what
|
|
180
|
+
an upstream that was force-pushed needs — otherwise you land on the copy the
|
|
181
|
+
last fetch happened to cache.
|
|
182
|
+
|
|
183
|
+
The summary counts what each repo loses before anything runs, and flags the
|
|
184
|
+
commits no remote carries — the only ones a reset makes unrecoverable outside
|
|
185
|
+
the reflog. The confirmation defaults to no; `-y/--yes` skips it.
|
|
186
|
+
|
|
187
|
+
A repo is skipped, and the run exits non-zero, when a git operation is already
|
|
188
|
+
in progress, when the worktree is missing, when its refs will not resolve
|
|
189
|
+
locally, or when it is not on the branch the config names. That last one
|
|
190
|
+
matters: resetting whatever else happens to be checked out would throw away
|
|
191
|
+
work ow was never told about, and realigning is `ow apply`'s job.
|
|
192
|
+
|
|
114
193
|
## `ow prune`
|
|
115
194
|
|
|
116
195
|
Cleans up stale worktree references and orphaned local branches from every bare repo, and drops
|
|
@@ -30,6 +30,7 @@ src/odoo_workspaces.egg-info/top_level.txt
|
|
|
30
30
|
src/ow/__init__.py
|
|
31
31
|
src/ow/__main__.py
|
|
32
32
|
src/ow/_version.py
|
|
33
|
+
src/ow/askpass.py
|
|
33
34
|
src/ow/_static/services/compose.yml.j2
|
|
34
35
|
src/ow/_static/templates/bwrap/bwrap-claude.j2
|
|
35
36
|
src/ow/_static/templates/bwrap/bwrap-opencode.j2
|
|
@@ -51,11 +52,14 @@ src/ow/commands/ls.py
|
|
|
51
52
|
src/ow/commands/mv.py
|
|
52
53
|
src/ow/commands/open.py
|
|
53
54
|
src/ow/commands/prune.py
|
|
55
|
+
src/ow/commands/pull.py
|
|
54
56
|
src/ow/commands/rebase.py
|
|
57
|
+
src/ow/commands/reset.py
|
|
55
58
|
src/ow/commands/rm.py
|
|
56
59
|
src/ow/commands/shell_init.py
|
|
57
60
|
src/ow/commands/status.py
|
|
58
61
|
src/ow/commands/templates.py
|
|
62
|
+
src/ow/utils/askpass.py
|
|
59
63
|
src/ow/utils/config.py
|
|
60
64
|
src/ow/utils/display.py
|
|
61
65
|
src/ow/utils/drift.py
|
|
@@ -63,9 +67,11 @@ src/ow/utils/git.py
|
|
|
63
67
|
src/ow/utils/index.py
|
|
64
68
|
src/ow/utils/legacy.py
|
|
65
69
|
src/ow/utils/paths.py
|
|
70
|
+
src/ow/utils/pull_plan.py
|
|
66
71
|
src/ow/utils/rebase_plan.py
|
|
67
72
|
src/ow/utils/refs.py
|
|
68
73
|
src/ow/utils/relocate.py
|
|
74
|
+
src/ow/utils/reset_plan.py
|
|
69
75
|
src/ow/utils/resolver.py
|
|
70
76
|
src/ow/utils/templates.py
|
|
71
77
|
src/tests/__init__.py
|
|
@@ -82,8 +88,10 @@ src/tests/commands/test_misc_extended.py
|
|
|
82
88
|
src/tests/commands/test_mv.py
|
|
83
89
|
src/tests/commands/test_open.py
|
|
84
90
|
src/tests/commands/test_prune.py
|
|
91
|
+
src/tests/commands/test_pull.py
|
|
85
92
|
src/tests/commands/test_rebase.py
|
|
86
93
|
src/tests/commands/test_rebase_scenarios.py
|
|
94
|
+
src/tests/commands/test_reset.py
|
|
87
95
|
src/tests/commands/test_rm.py
|
|
88
96
|
src/tests/commands/test_shell_init.py
|
|
89
97
|
src/tests/commands/test_status.py
|
|
@@ -91,6 +99,7 @@ src/tests/commands/test_status_extended.py
|
|
|
91
99
|
src/tests/commands/test_status_extended2.py
|
|
92
100
|
src/tests/commands/test_templates_cmd.py
|
|
93
101
|
src/tests/utils/__init__.py
|
|
102
|
+
src/tests/utils/test_askpass.py
|
|
94
103
|
src/tests/utils/test_config.py
|
|
95
104
|
src/tests/utils/test_display.py
|
|
96
105
|
src/tests/utils/test_drift.py
|
|
@@ -102,8 +111,10 @@ src/tests/utils/test_legacy.py
|
|
|
102
111
|
src/tests/utils/test_misc.py
|
|
103
112
|
src/tests/utils/test_paths.py
|
|
104
113
|
src/tests/utils/test_progress.py
|
|
114
|
+
src/tests/utils/test_pull_plan.py
|
|
105
115
|
src/tests/utils/test_rebase_plan.py
|
|
106
116
|
src/tests/utils/test_refs.py
|
|
117
|
+
src/tests/utils/test_reset_plan.py
|
|
107
118
|
src/tests/utils/test_resolve_template_files.py
|
|
108
119
|
src/tests/utils/test_resolver.py
|
|
109
120
|
src/tests/utils/test_templates.py
|
{odoo_workspaces-2.1.0 → odoo_workspaces-2.2.0}/src/odoo_workspaces.egg-info/scm_file_list.json
RENAMED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"src/ow/_static/templates/vscode/.vscode/settings.json.j2",
|
|
36
36
|
"src/ow/_static/templates/zed/.zed/debug.json.j2",
|
|
37
37
|
"src/ow/_static/templates/zed/.zed/settings.json.j2",
|
|
38
|
+
"src/ow/askpass.py",
|
|
38
39
|
"src/ow/commands/__init__.py",
|
|
39
40
|
"src/ow/commands/apply.py",
|
|
40
41
|
"src/ow/commands/archive.py",
|
|
@@ -44,11 +45,14 @@
|
|
|
44
45
|
"src/ow/commands/mv.py",
|
|
45
46
|
"src/ow/commands/open.py",
|
|
46
47
|
"src/ow/commands/prune.py",
|
|
48
|
+
"src/ow/commands/pull.py",
|
|
47
49
|
"src/ow/commands/rebase.py",
|
|
50
|
+
"src/ow/commands/reset.py",
|
|
48
51
|
"src/ow/commands/rm.py",
|
|
49
52
|
"src/ow/commands/shell_init.py",
|
|
50
53
|
"src/ow/commands/status.py",
|
|
51
54
|
"src/ow/commands/templates.py",
|
|
55
|
+
"src/ow/utils/askpass.py",
|
|
52
56
|
"src/ow/utils/config.py",
|
|
53
57
|
"src/ow/utils/display.py",
|
|
54
58
|
"src/ow/utils/drift.py",
|
|
@@ -56,9 +60,11 @@
|
|
|
56
60
|
"src/ow/utils/index.py",
|
|
57
61
|
"src/ow/utils/legacy.py",
|
|
58
62
|
"src/ow/utils/paths.py",
|
|
63
|
+
"src/ow/utils/pull_plan.py",
|
|
59
64
|
"src/ow/utils/rebase_plan.py",
|
|
60
65
|
"src/ow/utils/refs.py",
|
|
61
66
|
"src/ow/utils/relocate.py",
|
|
67
|
+
"src/ow/utils/reset_plan.py",
|
|
62
68
|
"src/ow/utils/resolver.py",
|
|
63
69
|
"src/ow/utils/templates.py",
|
|
64
70
|
"src/tests/__init__.py",
|
|
@@ -72,8 +78,10 @@
|
|
|
72
78
|
"src/tests/commands/test_mv.py",
|
|
73
79
|
"src/tests/commands/test_open.py",
|
|
74
80
|
"src/tests/commands/test_prune.py",
|
|
81
|
+
"src/tests/commands/test_pull.py",
|
|
75
82
|
"src/tests/commands/test_rebase.py",
|
|
76
83
|
"src/tests/commands/test_rebase_scenarios.py",
|
|
84
|
+
"src/tests/commands/test_reset.py",
|
|
77
85
|
"src/tests/commands/test_rm.py",
|
|
78
86
|
"src/tests/commands/test_shell_init.py",
|
|
79
87
|
"src/tests/commands/test_status.py",
|
|
@@ -84,6 +92,7 @@
|
|
|
84
92
|
"src/tests/test_cli.py",
|
|
85
93
|
"src/tests/test_main_extended.py",
|
|
86
94
|
"src/tests/utils/__init__.py",
|
|
95
|
+
"src/tests/utils/test_askpass.py",
|
|
87
96
|
"src/tests/utils/test_config.py",
|
|
88
97
|
"src/tests/utils/test_display.py",
|
|
89
98
|
"src/tests/utils/test_drift.py",
|
|
@@ -95,8 +104,10 @@
|
|
|
95
104
|
"src/tests/utils/test_misc.py",
|
|
96
105
|
"src/tests/utils/test_paths.py",
|
|
97
106
|
"src/tests/utils/test_progress.py",
|
|
107
|
+
"src/tests/utils/test_pull_plan.py",
|
|
98
108
|
"src/tests/utils/test_rebase_plan.py",
|
|
99
109
|
"src/tests/utils/test_refs.py",
|
|
110
|
+
"src/tests/utils/test_reset_plan.py",
|
|
100
111
|
"src/tests/utils/test_resolve_template_files.py",
|
|
101
112
|
"src/tests/utils/test_resolver.py",
|
|
102
113
|
"src/tests/utils/test_templates.py",
|
|
@@ -14,14 +14,16 @@ from ow.commands import (
|
|
|
14
14
|
cmd_mv,
|
|
15
15
|
cmd_open,
|
|
16
16
|
cmd_prune,
|
|
17
|
+
cmd_pull,
|
|
17
18
|
cmd_rebase,
|
|
19
|
+
cmd_reset,
|
|
18
20
|
cmd_rm,
|
|
19
21
|
cmd_shell_init,
|
|
20
22
|
cmd_status,
|
|
21
23
|
cmd_templates,
|
|
22
24
|
cmd_unarchive,
|
|
23
25
|
)
|
|
24
|
-
from ow.utils import index
|
|
26
|
+
from ow.utils import askpass, index
|
|
25
27
|
from ow.utils.config import Config, load_global_config, parse_branch_spec
|
|
26
28
|
from ow.utils.display import err_console
|
|
27
29
|
from ow.utils.legacy import check_legacy_layout
|
|
@@ -220,6 +222,31 @@ def rebase(
|
|
|
220
222
|
)
|
|
221
223
|
|
|
222
224
|
|
|
225
|
+
@app.command()
|
|
226
|
+
def pull(
|
|
227
|
+
workspace: Optional[str] = typer.Argument(None, help=WORKSPACE_HELP, autocompletion=complete_workspace_name),
|
|
228
|
+
only: Optional[str] = typer.Option(None, "--only", help="Comma-separated repo aliases to pull (default: all)"),
|
|
229
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Show the git commands without running them"),
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Fetch and fast-forward workspace branches."""
|
|
232
|
+
config = _load_config()
|
|
233
|
+
cmd_pull(config, workspace=workspace, only=only, dry_run=dry_run)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@app.command()
|
|
237
|
+
def reset(
|
|
238
|
+
workspace: Optional[str] = typer.Argument(None, help=WORKSPACE_HELP, autocompletion=complete_workspace_name),
|
|
239
|
+
only: Optional[str] = typer.Option(None, "--only", help="Comma-separated repo aliases to reset (default: all)"),
|
|
240
|
+
hard: bool = typer.Option(False, "--hard", help="Discard the working tree too, not just the commits"),
|
|
241
|
+
fetch: bool = typer.Option(False, "--fetch", "-f", help="Refresh the refs first, instead of resetting to what is already cached"),
|
|
242
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Show the git commands without running them"),
|
|
243
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt"),
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Put workspace repos back on the refs they follow."""
|
|
246
|
+
config = _load_config()
|
|
247
|
+
cmd_reset(config, workspace=workspace, only=only, hard=hard, fetch=fetch, dry_run=dry_run, yes=yes)
|
|
248
|
+
|
|
249
|
+
|
|
223
250
|
@app.command()
|
|
224
251
|
def ls(
|
|
225
252
|
archived: bool = typer.Option(False, "--archived", help="List archived workspaces instead of active ones"),
|
|
@@ -323,7 +350,8 @@ def open_ws(
|
|
|
323
350
|
|
|
324
351
|
def main() -> None:
|
|
325
352
|
try:
|
|
326
|
-
|
|
353
|
+
with askpass.broker():
|
|
354
|
+
app()
|
|
327
355
|
except KeyboardInterrupt:
|
|
328
356
|
# 130 is the conventional shell status for SIGINT. The git children
|
|
329
357
|
# are already gone, pool or no pool: each one runs in its own session
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 2, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g2cc2fc2b1'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""SSH_ASKPASS shim: ask the ow process that spawned git, print the answer.
|
|
2
|
+
|
|
3
|
+
ssh execs this with the prompt as its first argument and reads the secret
|
|
4
|
+
from its stdout. It runs in the git child's session, with no terminal of
|
|
5
|
+
its own, so it forwards the question to the broker in ow — which still has
|
|
6
|
+
one. See ow.utils.askpass.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import os
|
|
11
|
+
import socket
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> int:
|
|
16
|
+
sock_path = os.environ.get("OW_ASKPASS_SOCK")
|
|
17
|
+
if not sock_path:
|
|
18
|
+
return 1
|
|
19
|
+
|
|
20
|
+
prompt = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
21
|
+
try:
|
|
22
|
+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as conn:
|
|
23
|
+
conn.connect(sock_path)
|
|
24
|
+
conn.sendall(base64.b64encode(prompt.encode("utf-8")) + b"\n")
|
|
25
|
+
chunks: list[bytes] = []
|
|
26
|
+
while not chunks or not chunks[-1].endswith(b"\n"):
|
|
27
|
+
chunk = conn.recv(4096)
|
|
28
|
+
if not chunk:
|
|
29
|
+
break
|
|
30
|
+
chunks.append(chunk)
|
|
31
|
+
except OSError:
|
|
32
|
+
return 1
|
|
33
|
+
|
|
34
|
+
payload = b"".join(chunks).strip()
|
|
35
|
+
answer = base64.b64decode(payload).decode("utf-8", "replace") if payload else ""
|
|
36
|
+
sys.stdout.write(answer + "\n")
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
sys.exit(main())
|
|
@@ -6,7 +6,9 @@ from ow.commands.ls import cmd_ls
|
|
|
6
6
|
from ow.commands.mv import cmd_mv
|
|
7
7
|
from ow.commands.open import cmd_open
|
|
8
8
|
from ow.commands.prune import cmd_prune
|
|
9
|
+
from ow.commands.pull import cmd_pull
|
|
9
10
|
from ow.commands.rebase import cmd_rebase
|
|
11
|
+
from ow.commands.reset import cmd_reset
|
|
10
12
|
from ow.commands.rm import cmd_rm
|
|
11
13
|
from ow.commands.shell_init import cmd_shell_init
|
|
12
14
|
from ow.commands.status import cmd_status
|
|
@@ -21,7 +23,9 @@ __all__ = [
|
|
|
21
23
|
"cmd_mv",
|
|
22
24
|
"cmd_open",
|
|
23
25
|
"cmd_prune",
|
|
26
|
+
"cmd_pull",
|
|
24
27
|
"cmd_rebase",
|
|
28
|
+
"cmd_reset",
|
|
25
29
|
"cmd_rm",
|
|
26
30
|
"cmd_shell_init",
|
|
27
31
|
"cmd_status",
|
|
@@ -4,7 +4,8 @@ from typing import NamedTuple
|
|
|
4
4
|
|
|
5
5
|
from ow.utils.display import confirm, err_console
|
|
6
6
|
from ow.utils import index, paths
|
|
7
|
-
from ow.utils.
|
|
7
|
+
from ow.utils.config import load_workspace_config
|
|
8
|
+
from ow.utils.git import _run, get_worktree_branch, is_branch_pushed, parallel_per_repo
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class _PrunePlan(NamedTuple):
|
|
@@ -51,7 +52,8 @@ def _survey_worktrees(bare_repo: Path) -> tuple[set[str], list[str]]:
|
|
|
51
52
|
|
|
52
53
|
A prunable worktree's branch is deliberately not counted as in use: it
|
|
53
54
|
is about to stop being attached to anything, and the branch pass has to
|
|
54
|
-
see the repo as it will be, not as it was.
|
|
55
|
+
see the repo as it will be, not as it was. What keeps that from eating a
|
|
56
|
+
live workspace's branch is _declared_branches(), not this listing.
|
|
55
57
|
"""
|
|
56
58
|
result = _run(
|
|
57
59
|
["git", "-C", str(bare_repo), "worktree", "list", "--porcelain"],
|
|
@@ -60,7 +62,12 @@ def _survey_worktrees(bare_repo: Path) -> tuple[set[str], list[str]]:
|
|
|
60
62
|
used: set[str] = set()
|
|
61
63
|
stale: list[str] = []
|
|
62
64
|
if result.returncode != 0:
|
|
63
|
-
|
|
65
|
+
# Not "nothing is in use": that reading turns every branch in the
|
|
66
|
+
# repo into an orphan candidate, which is the one direction this
|
|
67
|
+
# command must never fail in.
|
|
68
|
+
raise RuntimeError(
|
|
69
|
+
f"git worktree list failed: {result.stderr.strip() or 'no output'}"
|
|
70
|
+
)
|
|
64
71
|
|
|
65
72
|
path: str | None = None
|
|
66
73
|
branch: str | None = None
|
|
@@ -92,7 +99,57 @@ def _survey_worktrees(bare_repo: Path) -> tuple[set[str], list[str]]:
|
|
|
92
99
|
return used, stale
|
|
93
100
|
|
|
94
101
|
|
|
95
|
-
def
|
|
102
|
+
def _declared_branches() -> tuple[dict[str, set[str]], list[tuple[Path, str]]]:
|
|
103
|
+
"""Per alias, the branches live workspaces own. Plus the configs that would not read.
|
|
104
|
+
|
|
105
|
+
git's worktree bookkeeping is not the only truth. A registration that
|
|
106
|
+
went stale — a directory moved by hand, a mount that was away when the
|
|
107
|
+
listing ran — makes git call the worktree prunable and its branch
|
|
108
|
+
unattached, while the workspace is still there and still says in its own
|
|
109
|
+
.ow/config.toml which branch it works on. Issue #50: prune offered to
|
|
110
|
+
delete exactly such a branch, and git refused it as in use.
|
|
111
|
+
|
|
112
|
+
Both the declared branch and the branch actually checked out are taken:
|
|
113
|
+
a worktree moved to a branch the config does not name is still work.
|
|
114
|
+
"""
|
|
115
|
+
declared: dict[str, set[str]] = {}
|
|
116
|
+
unreadable: list[tuple[Path, str]] = []
|
|
117
|
+
|
|
118
|
+
# list_workspaces, not known_workspaces: the latter rewrites the index
|
|
119
|
+
# as it reads, and --dry-run and a declined confirmation must leave it
|
|
120
|
+
# exactly as they found it. Dead entries are dropped later, by the index
|
|
121
|
+
# pass that already owns that job.
|
|
122
|
+
for ws_dir in index.list_workspaces():
|
|
123
|
+
try:
|
|
124
|
+
config_file = ws_dir / ".ow" / "config.toml"
|
|
125
|
+
if not config_file.is_file():
|
|
126
|
+
# A dead index entry, not a workspace whose config is broken.
|
|
127
|
+
continue
|
|
128
|
+
ws = load_workspace_config(config_file)
|
|
129
|
+
owned_here: dict[str, set[str]] = {}
|
|
130
|
+
for alias, spec in ws.repos.items():
|
|
131
|
+
owned = owned_here.setdefault(alias, set())
|
|
132
|
+
if spec.local_branch:
|
|
133
|
+
# A detached spec owns no branch.
|
|
134
|
+
owned.add(spec.local_branch)
|
|
135
|
+
if (ws_dir / alias).is_dir():
|
|
136
|
+
checked_out = get_worktree_branch(ws_dir / alias)
|
|
137
|
+
if checked_out:
|
|
138
|
+
owned.add(checked_out)
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
# Unparseable, unreadable, on a directory that will not even
|
|
141
|
+
# stat: all the same answer — this workspace's branches cannot
|
|
142
|
+
# be named, so none of them can be shown to be orphaned.
|
|
143
|
+
unreadable.append((ws_dir, str(exc)))
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
for alias, owned in owned_here.items():
|
|
147
|
+
declared.setdefault(alias, set()).update(owned)
|
|
148
|
+
|
|
149
|
+
return declared, unreadable
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _survey_bare_repo(bare_repo: Path, protected: frozenset[str] = frozenset()) -> _PrunePlan:
|
|
96
153
|
"""Work out what would go from one bare repo, without touching it."""
|
|
97
154
|
used_branches, stale = _survey_worktrees(bare_repo)
|
|
98
155
|
|
|
@@ -117,7 +174,7 @@ def _survey_bare_repo(bare_repo: Path) -> _PrunePlan:
|
|
|
117
174
|
|
|
118
175
|
if branch_result.returncode == 0:
|
|
119
176
|
all_branches = {b.strip() for b in branch_result.stdout.splitlines() if b.strip()}
|
|
120
|
-
for branch in sorted(all_branches - used_branches):
|
|
177
|
+
for branch in sorted(all_branches - used_branches - protected):
|
|
121
178
|
if branch == head_branch:
|
|
122
179
|
# Never delete the branch HEAD points at — a dangling HEAD
|
|
123
180
|
# confuses every subsequent git command and looks like data loss.
|
|
@@ -268,14 +325,19 @@ def cmd_prune(*, dry_run: bool = False, yes: bool = False, also_backups: bool =
|
|
|
268
325
|
leaves everything untouched, the index included.
|
|
269
326
|
"""
|
|
270
327
|
dropped = _dead_index_entries()
|
|
328
|
+
declared, unreadable = _declared_branches()
|
|
271
329
|
|
|
272
330
|
bare_repos_dir = paths.repos_dir()
|
|
273
|
-
|
|
331
|
+
# Not every *.git under there is an alias: a stray `.git` directory
|
|
332
|
+
# matches the pattern too, and surveying it fails as "not a repository".
|
|
333
|
+
bare_repos = sorted(
|
|
334
|
+
p for p in bare_repos_dir.glob("*.git") if not p.name.startswith(".")
|
|
335
|
+
) if bare_repos_dir.exists() else []
|
|
274
336
|
if not bare_repos:
|
|
275
337
|
print("No bare repos found.")
|
|
276
338
|
|
|
277
339
|
surveyed = parallel_per_repo({
|
|
278
|
-
repo.stem: (lambda r=repo: _survey_bare_repo(r))
|
|
340
|
+
repo.stem: (lambda r=repo: _survey_bare_repo(r, frozenset(declared.get(r.stem, ()))))
|
|
279
341
|
for repo in bare_repos
|
|
280
342
|
})
|
|
281
343
|
plans: list[_PrunePlan] = []
|
|
@@ -291,6 +353,20 @@ def cmd_prune(*, dry_run: bool = False, yes: bool = False, also_backups: bool =
|
|
|
291
353
|
for alias, exc in survey_errors.items():
|
|
292
354
|
err_console.print(f" [{alias}] survey failed: {exc}", markup=False)
|
|
293
355
|
|
|
356
|
+
for ws_dir, reason in unreadable:
|
|
357
|
+
err_console.print(f" [{ws_dir.name}] unreadable .ow/config.toml: {reason}", markup=False)
|
|
358
|
+
if unreadable:
|
|
359
|
+
# Which branches that workspace owns is exactly what could not be
|
|
360
|
+
# read, so no branch can be shown to be orphaned. The reversible
|
|
361
|
+
# work — stale worktrees, the index, backups — still happens.
|
|
362
|
+
err_console.print(
|
|
363
|
+
" No branch will be deleted this run: ow cannot tell which branches that workspace owns."
|
|
364
|
+
)
|
|
365
|
+
plans = [
|
|
366
|
+
plan._replace(to_delete=[], kept=[*plan.kept, *plan.to_delete])
|
|
367
|
+
for plan in plans
|
|
368
|
+
]
|
|
369
|
+
|
|
294
370
|
_display_plan(plans)
|
|
295
371
|
backups = _stale_backups() if also_backups else []
|
|
296
372
|
_display_backups(backups)
|