odoo-workspaces 1.1.0__py3-none-any.whl
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-1.1.0.dist-info/METADATA +267 -0
- odoo_workspaces-1.1.0.dist-info/RECORD +33 -0
- odoo_workspaces-1.1.0.dist-info/WHEEL +5 -0
- odoo_workspaces-1.1.0.dist-info/entry_points.txt +2 -0
- odoo_workspaces-1.1.0.dist-info/licenses/LICENSE +21 -0
- odoo_workspaces-1.1.0.dist-info/top_level.txt +1 -0
- ow/__init__.py +0 -0
- ow/__main__.py +199 -0
- ow/_version.py +24 -0
- ow/config.py +129 -0
- ow/git.py +469 -0
- ow/services/compose.yml +31 -0
- ow/services/volumes/.gitkeep +4 -0
- ow/templates/bwrap/bwrap-claude.j2 +174 -0
- ow/templates/bwrap/bwrap-opencode.j2 +146 -0
- ow/templates/common/mise.toml.j2 +14 -0
- ow/templates/common/odools.toml.j2 +9 -0
- ow/templates/common/odoorc.j2 +12 -0
- ow/templates/common/pyrightconfig.json.j2 +12 -0
- ow/templates/common/requirements-dev.txt +1 -0
- ow/templates/vscode/.vscode/launch.json.j2 +37 -0
- ow/templates/vscode/.vscode/settings.json.j2 +3 -0
- ow/templates/zed/.zed/debug.json.j2 +40 -0
- ow/templates/zed/.zed/settings.json.j2 +11 -0
- ow/tests/__init__.py +0 -0
- ow/tests/conftest.py +100 -0
- ow/tests/test_cli.py +403 -0
- ow/tests/test_commands.py +638 -0
- ow/tests/test_config.py +279 -0
- ow/tests/test_git.py +1517 -0
- ow/tests/test_resolve_workspace.py +142 -0
- ow/tests/test_workspace.py +1130 -0
- ow/workspace.py +1588 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: odoo-workspaces
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Odoo workspace manager
|
|
5
|
+
Author-email: Bruno BOI <boi@odoo.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/brboi/ow
|
|
8
|
+
Project-URL: Repository, https://github.com/brboi/ow
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: argcomplete
|
|
21
|
+
Requires-Dist: jinja2
|
|
22
|
+
Requires-Dist: questionary
|
|
23
|
+
Requires-Dist: tomli-w
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# ow — Odoo Workspaces
|
|
27
|
+
|
|
28
|
+
CLI tool that turns interactive prompts into ready-to-code Odoo workspaces using git worktrees.
|
|
29
|
+
|
|
30
|
+
## Overview
|
|
31
|
+
|
|
32
|
+
- **Git Optimized Commands** — Clone Odoo repos in minutes using shared bare repos
|
|
33
|
+
- **Workspace generation** — each workspace is a folder with git worktrees and IDE configs, ready to open in VSCode or Zed
|
|
34
|
+
- **Interactive setup** — `ow create` guides you through name, templates, repos, and variables
|
|
35
|
+
- **Branch spec syntax** — concise `base..feature` notation to control detached vs attached worktrees
|
|
36
|
+
- **Shared bare repos** — all workspaces share the same `.bare-git-repos/`, so fetching once updates refs for all
|
|
37
|
+
- **Jinja2 template system** — generates `mise.toml`, `odoorc`, `odools.toml`, `pyrightconfig.json`, and IDE configs from customizable templates
|
|
38
|
+
- **Per-workspace variables** — global `[vars]` with per-workspace overrides for ports, DB credentials, etc.
|
|
39
|
+
- **Smart rebase** — two-step rebase (upstream then base), with conflict reporting and instructions
|
|
40
|
+
- **Rich status** — behind/ahead counts with color-coded output
|
|
41
|
+
- **Optional services** — Docker Compose stack with PostgreSQL, pgweb, and mailpit for local development
|
|
42
|
+
- **Tab completion** — fish, bash, zsh via `argcomplete`
|
|
43
|
+
- **Full transparency** — git commands that change your trees are printed to your terminal before execution
|
|
44
|
+
|
|
45
|
+
## Prerequisites
|
|
46
|
+
|
|
47
|
+
- **[mise](https://mise.jdx.dev/)** — manages Python, virtualenvs, and dependencies in generated workspaces
|
|
48
|
+
- **Odoo system dependencies** — see [Odoo source install docs](https://www.odoo.com/documentation/master/administration/on_premise/source.html#dependencies) (includes wkhtmltopdf, PostgreSQL client libs, etc.)
|
|
49
|
+
- **SSH** — configured for access to Odoo repositories
|
|
50
|
+
- **Docker or Podman** (optional) — to run services like postgres, pgweb, mailpit (see `services/`)
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
pipx install odoo-workspaces # recommended
|
|
56
|
+
pip install odoo-workspaces # or in an active venv
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
ow init # initialize project (ow.toml, templates/, services/)
|
|
63
|
+
# optionally edit ow.toml to add enterprise or dev remotes
|
|
64
|
+
ow create # interactive form: name, templates, repos, vars
|
|
65
|
+
cd workspaces/my_work && mise install
|
|
66
|
+
code workspaces/my_work # open in your IDE and enjoy
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Commands
|
|
70
|
+
|
|
71
|
+
| Command | Flags | Description |
|
|
72
|
+
|---------|-------|-------------|
|
|
73
|
+
| `ow init` | `--force`, `--force-with-backup` | Initialize a new ow project |
|
|
74
|
+
| `ow create` | `-n/--name`, `-t/--template`, `-r/--repo`, `-c/--configuration` | Create a workspace interactively |
|
|
75
|
+
| `ow update` | `[workspace]` | Re-render templates and materialize worktrees |
|
|
76
|
+
| `ow status` | `[workspace]` | Show branch status with behind/ahead counts |
|
|
77
|
+
| `ow rebase` | `[workspace]` | Fetch and rebase all repos in a workspace |
|
|
78
|
+
| `ow prune` | — | Clean up stale worktree references from bare repos |
|
|
79
|
+
|
|
80
|
+
`ow` walks up from the current directory to find `ow.toml` (the project root). Commands that need a workspace resolve it from the `OW_WORKSPACE` environment variable (set automatically by `mise`), or by walking up for `.ow/config`.
|
|
81
|
+
|
|
82
|
+
### `ow init`
|
|
83
|
+
|
|
84
|
+
Initializes a new ow project in the current directory:
|
|
85
|
+
|
|
86
|
+
- `ow.toml` — minimal config with the Odoo community remote
|
|
87
|
+
- `workspaces/` — empty directory for future workspaces
|
|
88
|
+
- `templates/` — copy of the bundled templates (customize freely)
|
|
89
|
+
- `mise.toml` — Python + Node tooling for ow itself
|
|
90
|
+
- `services/` — Docker Compose stack (postgres, pgweb, mailpit)
|
|
91
|
+
|
|
92
|
+
Use `--force` to overwrite existing files, or `--force-with-backup` to back them up first (`.bak` suffix).
|
|
93
|
+
|
|
94
|
+
### `ow create`
|
|
95
|
+
|
|
96
|
+
Interactive form: workspace name → template selection → repo aliases + branch specs → variable defaults. After confirmation:
|
|
97
|
+
|
|
98
|
+
1. Clones bare repos if needed
|
|
99
|
+
2. Fetches required refs
|
|
100
|
+
3. Creates worktrees
|
|
101
|
+
4. Applies templates in order
|
|
102
|
+
5. Writes `workspaces/<name>/.ow/config`
|
|
103
|
+
6. Trusts `mise.toml` and prints a reminder to run `mise install`
|
|
104
|
+
|
|
105
|
+
### `ow update`
|
|
106
|
+
|
|
107
|
+
Re-renders templates and materializes worktrees for the current workspace. Also creates any missing worktrees and merges new vars from `ow.toml`. Useful after template changes or to regenerate workspace files.
|
|
108
|
+
|
|
109
|
+
### `ow status`
|
|
110
|
+
|
|
111
|
+
Fetches latest refs and displays branch status with color-coded behind/ahead counts:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
[canary]
|
|
115
|
+
branches
|
|
116
|
+
community: dev/master-canary ↓0 ↑0 (origin/master ↓34 ↑0)
|
|
117
|
+
enterprise: dev/master-canary ↓1 ↑1 (origin/master ↓12 ↑0)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `ow rebase`
|
|
121
|
+
|
|
122
|
+
Fetches and rebases all repos in a workspace. Before executing, displays a summary of the rebase situation for each repo and asks for confirmation. Handles detached worktrees, force-pushed upstreams (with or without a recoverable fork-point), and normal two-step rebases.
|
|
123
|
+
|
|
124
|
+
### `ow prune`
|
|
125
|
+
|
|
126
|
+
Cleans up stale worktree references from all bare repos. Run after manually removing a workspace directory:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
rm -rf workspaces/my-workspace
|
|
130
|
+
ow prune
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Configuration (`ow.toml`)
|
|
134
|
+
|
|
135
|
+
### Remotes
|
|
136
|
+
|
|
137
|
+
```toml
|
|
138
|
+
[remotes]
|
|
139
|
+
community.origin.url = "git@github.com:odoo/odoo.git"
|
|
140
|
+
community.dev.url = "git@github.com:odoo-dev/odoo.git"
|
|
141
|
+
community.dev.pushurl = "git@github.com:odoo-dev/odoo.git"
|
|
142
|
+
community.dev.fetch = "+refs/heads/*:refs/remotes/dev/*"
|
|
143
|
+
|
|
144
|
+
enterprise.origin.url = "git@github.com:odoo/enterprise.git"
|
|
145
|
+
enterprise.dev.url = "git@github.com:odoo-dev/enterprise.git"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Each remote supports `url`, `pushurl` (optional), and `fetch` (optional refspec).
|
|
149
|
+
|
|
150
|
+
### Variables
|
|
151
|
+
|
|
152
|
+
```toml
|
|
153
|
+
[vars]
|
|
154
|
+
http_port = 8069
|
|
155
|
+
db_host = "localhost"
|
|
156
|
+
db_port = 5432
|
|
157
|
+
db_user = "odoo"
|
|
158
|
+
db_password = "odoo"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Templates use `{{ vars.key | default(fallback) }}` so undefined variables get safe defaults.
|
|
162
|
+
|
|
163
|
+
### Branch Spec Syntax
|
|
164
|
+
|
|
165
|
+
| Spec | Worktree mode |
|
|
166
|
+
|------|---------------|
|
|
167
|
+
| `master` | Detached HEAD at `origin/master` |
|
|
168
|
+
| `origin/master` | Detached HEAD at `origin/master` |
|
|
169
|
+
| `dev/master-phoenix` | Detached HEAD at `dev/master-phoenix` |
|
|
170
|
+
| `master..master-feature` | Attached local branch `master-feature` tracking `origin/master` |
|
|
171
|
+
| `dev/master-phoenix..fix` | Attached local branch `fix` tracking `dev/master-phoenix` |
|
|
172
|
+
|
|
173
|
+
Without `..`, the worktree is detached (read-only tracking). With `..`, a local branch is created — this is what you want for feature development.
|
|
174
|
+
|
|
175
|
+
## Template System
|
|
176
|
+
|
|
177
|
+
Templates live in `templates/` at the project root. Each subdirectory is a bundle that can be applied to workspaces during `ow create`. Bundles are applied in order — later ones override files from earlier ones.
|
|
178
|
+
|
|
179
|
+
| Bundle | Contents |
|
|
180
|
+
|--------|----------|
|
|
181
|
+
| `common/` | `mise.toml`, `odoorc`, `odools.toml`, `pyrightconfig.json`, `requirements-dev.txt` |
|
|
182
|
+
| `vscode/.vscode/` | `settings.json`, `launch.json` |
|
|
183
|
+
| `zed/.zed/` | `settings.json`, `debug.json` |
|
|
184
|
+
| `bwrap/` | Sandbox scripts for AI coding assistants |
|
|
185
|
+
|
|
186
|
+
Templates are Jinja2 (`.j2` extension); static files are copied as-is. `ow init` seeds `templates/` with the bundled defaults so you can customize them.
|
|
187
|
+
|
|
188
|
+
To create a custom bundle:
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
mkdir -p templates/my-setup
|
|
192
|
+
cp templates/common/odoorc.j2 templates/my-setup/
|
|
193
|
+
# edit templates/my-setup/odoorc.j2
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Then select it during `ow create` or add it to an existing workspace's `.ow/config`.
|
|
197
|
+
|
|
198
|
+
## Services
|
|
199
|
+
|
|
200
|
+
Optional containerized services for local development:
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
docker compose -f services/compose.yml up -d
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
| Service | Port | Description |
|
|
207
|
+
|---------|------|-------------|
|
|
208
|
+
| postgres | 5432 | PostgreSQL 17 with pgvector |
|
|
209
|
+
| pgweb | 8081 | Web-based PostgreSQL browser |
|
|
210
|
+
| mailpit | 8025 / 1025 | Email testing (web UI / SMTP) |
|
|
211
|
+
|
|
212
|
+
Configure your workspaces to use them via `[vars]` in `ow.toml`:
|
|
213
|
+
|
|
214
|
+
```toml
|
|
215
|
+
[vars]
|
|
216
|
+
db_host = "localhost"
|
|
217
|
+
db_port = 5432
|
|
218
|
+
smtp_server = "localhost"
|
|
219
|
+
smtp_port = 1025
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Tab Completion
|
|
223
|
+
|
|
224
|
+
Fish (one-time setup):
|
|
225
|
+
```sh
|
|
226
|
+
register-python-argcomplete --shell fish ow > ~/.config/fish/completions/ow.fish
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Bash/Zsh:
|
|
230
|
+
```sh
|
|
231
|
+
activate-global-python-argcomplete
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Sandboxing AI Coding Assistants
|
|
235
|
+
|
|
236
|
+
`ow` includes sandbox scripts for running AI coding assistants (Opencode, Claude Code) with filesystem isolation using [bubblewrap](https://github.com/containers/bubblewrap).
|
|
237
|
+
|
|
238
|
+
Install bubblewrap:
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
sudo apt install bubblewrap # Debian/Ubuntu
|
|
242
|
+
sudo dnf install bubblewrap # Fedora
|
|
243
|
+
sudo pacman -S bubblewrap # Arch
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Add `bwrap` to your workspace templates during `ow create`. The scripts are automatically added to PATH via `mise`:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
bwrap-opencode # Launch Opencode sandboxed
|
|
250
|
+
bwrap-claude # Launch Claude Code sandboxed
|
|
251
|
+
bwrap-opencode --add-dir ~/src/my-addon # grant access to an extra directory
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
To work on `ow` itself, use the scripts at the project root:
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
./bwrap-opencode # Launch Opencode sandboxed in ow directory
|
|
258
|
+
./bwrap-claude # Launch Claude Code sandboxed in ow directory
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Disclaimer
|
|
262
|
+
|
|
263
|
+
This is a small personal project built with the help of [Claude](https://claude.ai). It scratches a very specific itch — managing multiple Odoo worktrees side by side — and comes with no warranty. Use at your own risk, and expect rough edges.
|
|
264
|
+
|
|
265
|
+
## Want to contribute?
|
|
266
|
+
|
|
267
|
+
Contributions are welcome! If something is broken, confusing, or missing — open an issue. If you have a fix or improvement in mind, PRs are appreciated.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
odoo_workspaces-1.1.0.dist-info/licenses/LICENSE,sha256=dff2I-iCi_JfoTJMiVKI-ZoCmM1MGyRjYoIX7yyGfWU,1066
|
|
2
|
+
ow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
ow/__main__.py,sha256=nX3smmzh6e_U7whZr_JQ-SLDar2FC2gwaGqvRbdtsSM,6424
|
|
4
|
+
ow/_version.py,sha256=UX5-1P4_Li7oZPBPGeO0iysPZY4KtG9cYeh9ikqsnO4,520
|
|
5
|
+
ow/config.py,sha256=TJq-nwyBjBfl1qvqhNqq-xEojRwyXcwCaCai0g9u2jY,3628
|
|
6
|
+
ow/git.py,sha256=qNAGsms8-lkTV7PbwnbMY3wT0KfBAklFxzj4f5ztKoY,17645
|
|
7
|
+
ow/workspace.py,sha256=MIOEQ8U86k90a_8j71mlUdFuYRxu3WN_G13Oe4tBxqg,57488
|
|
8
|
+
ow/services/compose.yml,sha256=kRJO29fSaZicIeaT8v4BKn0NSTJlbZ8_AxofEwHGi4o,699
|
|
9
|
+
ow/services/volumes/.gitkeep,sha256=0p5uq5enOuX4NLuFg8Q2bhmdV5OusKALZw9GQMs_e4I,255
|
|
10
|
+
ow/templates/bwrap/bwrap-claude.j2,sha256=D-atf4VKoyU8whgF9VR8czPDI1wdlBdLub2QfvXj0kU,5858
|
|
11
|
+
ow/templates/bwrap/bwrap-opencode.j2,sha256=xibjHuKWRHCnZmfgllLFUXKxbfmChqfWxNbLxVhRpIA,4828
|
|
12
|
+
ow/templates/common/mise.toml.j2,sha256=AS70CEUMFKhFmpQq6gG16OiHk1DoySusuJ06FNiHD30,554
|
|
13
|
+
ow/templates/common/odools.toml.j2,sha256=8MDq27P7-HJSkZtMbB2QR80F8OOk87rfF9KD5ylpeig,210
|
|
14
|
+
ow/templates/common/odoorc.j2,sha256=XoxsEgWNWw8R8343Ld_P1zxQK7yU9kFPUusUQsCYH68,512
|
|
15
|
+
ow/templates/common/pyrightconfig.json.j2,sha256=7UCyqIdJcYTwxSppNigcc4JNgd-XVcCB4HM-FZjrX9c,330
|
|
16
|
+
ow/templates/common/requirements-dev.txt,sha256=-azvCdZIbby4JA5EpvHLG644USUwZNih1nrj4aguklY,8
|
|
17
|
+
ow/templates/vscode/.vscode/launch.json.j2,sha256=iHSiNsVqsKgFFKmZVEhVkqfbNooFqrptOTTgE7x79_g,1238
|
|
18
|
+
ow/templates/vscode/.vscode/settings.json.j2,sha256=HicLCSRdGTEOEvgb1lp7pyc4bF0zt2dyxzuSKSazPtc,64
|
|
19
|
+
ow/templates/zed/.zed/debug.json.j2,sha256=jxK82q0yLGgLaCEvuer-beXaHX2WJuiHeC4Lrcc0D88,1356
|
|
20
|
+
ow/templates/zed/.zed/settings.json.j2,sha256=QHeX_dngaQXvGDopRq25vrwqp9QCl5i5ylP8Fy7lHfs,467
|
|
21
|
+
ow/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
ow/tests/conftest.py,sha256=N9R5qdh2hgHhBKh9BZaTKVEtfUOM2DB77nYENZLlqZg,3102
|
|
23
|
+
ow/tests/test_cli.py,sha256=BwNPYh0C3Vxfqf984C92_QuUsqDMmxQVPvGIv6OewvY,13273
|
|
24
|
+
ow/tests/test_commands.py,sha256=z0t0GQboIa50BSqQebbZncGQRsTWJ1yAZma7lqH3-Eg,25259
|
|
25
|
+
ow/tests/test_config.py,sha256=9RZ9yF6knVoWWQAiCCPco3VHgmxFA2pUuiAetuAwGpo,8585
|
|
26
|
+
ow/tests/test_git.py,sha256=_MZRSjod84OZiHuVcqALfbEouRQ4q4VHaA0DkqywrXM,53117
|
|
27
|
+
ow/tests/test_resolve_workspace.py,sha256=e86CEGabDHb_z2SZTBG9u_Ep3PBUix62lWDg86xSgsw,4972
|
|
28
|
+
ow/tests/test_workspace.py,sha256=QljlZJCOl703Z52jKHlswcsFL3PBZDbtgx57vEbTNao,41855
|
|
29
|
+
odoo_workspaces-1.1.0.dist-info/METADATA,sha256=TsiL1BDK-HJzf6xYEMhtHJxn6TELngvrGSJhP7P2BQ0,10109
|
|
30
|
+
odoo_workspaces-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
31
|
+
odoo_workspaces-1.1.0.dist-info/entry_points.txt,sha256=B-50333hmy1ubgaRVFLYx9TgweccCBemBGwV6sKulVs,40
|
|
32
|
+
odoo_workspaces-1.1.0.dist-info/top_level.txt,sha256=J_l1r4EMChUJYIniVsADjPxnH3tNme05AuTLZCxb8ZM,3
|
|
33
|
+
odoo_workspaces-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bruno BOI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ow
|
ow/__init__.py
ADDED
|
File without changes
|
ow/__main__.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import argcomplete
|
|
10
|
+
|
|
11
|
+
from ow.config import Config, load_config, parse_branch_spec
|
|
12
|
+
from ow.workspace import (
|
|
13
|
+
_available_templates,
|
|
14
|
+
cmd_create,
|
|
15
|
+
cmd_init,
|
|
16
|
+
cmd_prune,
|
|
17
|
+
cmd_rebase,
|
|
18
|
+
cmd_status,
|
|
19
|
+
cmd_update,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from ow._version import version as __version__
|
|
24
|
+
except ImportError:
|
|
25
|
+
__version__ = "dev"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_root() -> Path:
|
|
29
|
+
current = Path.cwd()
|
|
30
|
+
while True:
|
|
31
|
+
if (current / "ow.toml").exists() or (current / "ow.toml.example").exists():
|
|
32
|
+
return current
|
|
33
|
+
if current.parent == current:
|
|
34
|
+
raise FileNotFoundError(
|
|
35
|
+
"ow.toml not found in current directory or any parent"
|
|
36
|
+
)
|
|
37
|
+
current = current.parent
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _available_repo_aliases():
|
|
41
|
+
"""Return repo aliases from ow.toml in declaration order."""
|
|
42
|
+
try:
|
|
43
|
+
root = find_root()
|
|
44
|
+
toml_path = root / "ow.toml"
|
|
45
|
+
if toml_path.exists():
|
|
46
|
+
cfg = load_config(toml_path)
|
|
47
|
+
return list(cfg.remotes.keys())
|
|
48
|
+
except FileNotFoundError:
|
|
49
|
+
pass
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _complete_gen_templates(prefix, parsed_args, **kwargs):
|
|
54
|
+
"""Tab completion for -t/--template."""
|
|
55
|
+
try:
|
|
56
|
+
root = find_root()
|
|
57
|
+
config = Config(vars={}, remotes={}, root_dir=root)
|
|
58
|
+
templates = _available_templates(config)
|
|
59
|
+
except (FileNotFoundError, Exception):
|
|
60
|
+
templates = []
|
|
61
|
+
return [t for t in templates if t.startswith(prefix)]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _complete_gen_repos(prefix, parsed_args, **kwargs):
|
|
65
|
+
"""Tab completion for -r/--repo — offers unused aliases."""
|
|
66
|
+
provided_aliases = set()
|
|
67
|
+
if parsed_args.repo:
|
|
68
|
+
for pair in parsed_args.repo:
|
|
69
|
+
provided_aliases.add(pair[0])
|
|
70
|
+
available = [a for a in _available_repo_aliases() if a not in provided_aliases]
|
|
71
|
+
if available:
|
|
72
|
+
return [a for a in available if a.startswith(prefix)]
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _complete_workspace_name(prefix, parsed_args, **kwargs):
|
|
77
|
+
"""Tab completion for workspace name."""
|
|
78
|
+
try:
|
|
79
|
+
root = find_root()
|
|
80
|
+
workspaces_dir = root / "workspaces"
|
|
81
|
+
if workspaces_dir.exists():
|
|
82
|
+
names = [d.name for d in workspaces_dir.iterdir() if d.is_dir() and (d / ".ow" / "config").exists()]
|
|
83
|
+
return [n for n in names if n.startswith(prefix)]
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
pass
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main() -> None:
|
|
90
|
+
parser = argparse.ArgumentParser(prog="ow", description="Odoo workspace manager")
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
93
|
+
)
|
|
94
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
95
|
+
|
|
96
|
+
p_create = subparsers.add_parser(
|
|
97
|
+
"create", help="Create a new workspace"
|
|
98
|
+
)
|
|
99
|
+
p_create.add_argument(
|
|
100
|
+
"-n", "--name", help="Workspace name"
|
|
101
|
+
)
|
|
102
|
+
p_create.add_argument(
|
|
103
|
+
"-c", "--configuration", help="Path to existing workspace config to duplicate"
|
|
104
|
+
)
|
|
105
|
+
p_create.add_argument(
|
|
106
|
+
"-t", "--template", nargs="*", metavar="TEMPLATE",
|
|
107
|
+
help="Templates to apply (space-separated)",
|
|
108
|
+
).completer = _complete_gen_templates # type: ignore[attr-defined]
|
|
109
|
+
p_create.add_argument(
|
|
110
|
+
"-r", "--repo", nargs=2, action="append", metavar=("ALIAS", "SPEC"),
|
|
111
|
+
help="Repo alias and branch spec (repeatable, e.g. -r community master..x)",
|
|
112
|
+
).completer = _complete_gen_repos # type: ignore[attr-defined]
|
|
113
|
+
|
|
114
|
+
p_update = subparsers.add_parser(
|
|
115
|
+
"update", help="Re-render templates and materialize worktrees"
|
|
116
|
+
)
|
|
117
|
+
p_update.add_argument(
|
|
118
|
+
"workspace", nargs="?", help="Workspace name (default: resolve from cwd)"
|
|
119
|
+
).completer = _complete_workspace_name # type: ignore[attr-defined]
|
|
120
|
+
|
|
121
|
+
p_status = subparsers.add_parser("status", help="Show workspace status")
|
|
122
|
+
p_status.add_argument(
|
|
123
|
+
"workspace", nargs="?", help="Workspace name (default: resolve from cwd)"
|
|
124
|
+
).completer = _complete_workspace_name # type: ignore[attr-defined]
|
|
125
|
+
|
|
126
|
+
p_rebase = subparsers.add_parser(
|
|
127
|
+
"rebase", help="Fetch and rebase workspace branches"
|
|
128
|
+
)
|
|
129
|
+
p_rebase.add_argument(
|
|
130
|
+
"workspace", nargs="?", help="Workspace name (default: resolve from cwd)"
|
|
131
|
+
).completer = _complete_workspace_name # type: ignore[attr-defined]
|
|
132
|
+
|
|
133
|
+
p_prune = subparsers.add_parser(
|
|
134
|
+
"prune", help="Clean up stale worktree references and orphaned branches"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
p_init = subparsers.add_parser(
|
|
138
|
+
"init", help="Initialize a new ow project"
|
|
139
|
+
)
|
|
140
|
+
p_init.add_argument(
|
|
141
|
+
"--force", action="store_true",
|
|
142
|
+
help="Overwrite existing files without backup"
|
|
143
|
+
)
|
|
144
|
+
p_init.add_argument(
|
|
145
|
+
"--force-with-backup", action="store_true",
|
|
146
|
+
help="Backup existing files before overwrite"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Tab completion for create subcommand
|
|
150
|
+
argcomplete.autocomplete(parser)
|
|
151
|
+
args = parser.parse_args()
|
|
152
|
+
|
|
153
|
+
# Handle 'init' command separately (doesn't require existing ow.toml)
|
|
154
|
+
if args.command == "init":
|
|
155
|
+
cmd_init(
|
|
156
|
+
path=None,
|
|
157
|
+
force=args.force,
|
|
158
|
+
with_backup=args.force_with_backup
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# All other commands require an existing ow.toml
|
|
163
|
+
try:
|
|
164
|
+
root = find_root()
|
|
165
|
+
except FileNotFoundError as e:
|
|
166
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
|
|
169
|
+
toml_path = root / "ow.toml"
|
|
170
|
+
if not toml_path.exists():
|
|
171
|
+
minimal_config = """\
|
|
172
|
+
[remotes]
|
|
173
|
+
community.origin.url = "git@github.com:odoo/odoo.git"
|
|
174
|
+
"""
|
|
175
|
+
toml_path.write_text(minimal_config)
|
|
176
|
+
print("Created ow.toml with default configuration. Edit it to suit your needs.")
|
|
177
|
+
|
|
178
|
+
config = load_config(toml_path)
|
|
179
|
+
|
|
180
|
+
if args.command == "create":
|
|
181
|
+
# Parse repo pairs from --repo args
|
|
182
|
+
repo_pairs = {}
|
|
183
|
+
if args.repo:
|
|
184
|
+
for alias, spec in args.repo:
|
|
185
|
+
repo_pairs[alias] = parse_branch_spec(spec)
|
|
186
|
+
|
|
187
|
+
cmd_create(config, name=args.name, templates=args.template, repos=repo_pairs, configuration=args.configuration)
|
|
188
|
+
elif args.command == "update":
|
|
189
|
+
cmd_update(config, workspace=args.workspace)
|
|
190
|
+
elif args.command == "status":
|
|
191
|
+
cmd_status(config, workspace=args.workspace)
|
|
192
|
+
elif args.command == "rebase":
|
|
193
|
+
cmd_rebase(config, workspace=args.workspace)
|
|
194
|
+
elif args.command == "prune":
|
|
195
|
+
cmd_prune(config)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
main()
|
ow/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
ow/config.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import tomllib
|
|
5
|
+
import tomli_w
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class BranchSpec:
|
|
13
|
+
base_ref: str # e.g. "origin/master", "dev/master-phoenix"
|
|
14
|
+
local_branch: str | None = None # None = detached
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def is_detached(self) -> bool:
|
|
18
|
+
return self.local_branch is None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def remote(self) -> str:
|
|
22
|
+
return self.base_ref.split("/")[0]
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def branch(self) -> str:
|
|
26
|
+
return "/".join(self.base_ref.split("/")[1:])
|
|
27
|
+
|
|
28
|
+
def to_spec_str(self) -> str:
|
|
29
|
+
base = self.branch if self.remote == "origin" else self.base_ref
|
|
30
|
+
if self.local_branch is None:
|
|
31
|
+
return base
|
|
32
|
+
return f"{base}..{self.local_branch}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_branch_spec(spec: str) -> BranchSpec:
|
|
36
|
+
"""
|
|
37
|
+
"master" → BranchSpec("origin/master")
|
|
38
|
+
"master..master-feature" → BranchSpec("origin/master", "master-feature")
|
|
39
|
+
"dev/master-phoenix..fix" → BranchSpec("dev/master-phoenix", "fix")
|
|
40
|
+
"origin/master" → BranchSpec("origin/master")
|
|
41
|
+
"""
|
|
42
|
+
if ".." in spec:
|
|
43
|
+
base, local = spec.split("..", 1)
|
|
44
|
+
if "/" not in base:
|
|
45
|
+
base = f"origin/{base}"
|
|
46
|
+
return BranchSpec(base, local)
|
|
47
|
+
if "/" in spec:
|
|
48
|
+
return BranchSpec(spec)
|
|
49
|
+
return BranchSpec(f"origin/{spec}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class RemoteConfig:
|
|
54
|
+
url: str
|
|
55
|
+
pushurl: str | None = None
|
|
56
|
+
fetch: str | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class WorkspaceConfig:
|
|
61
|
+
repos: dict[str, BranchSpec]
|
|
62
|
+
templates: list[str]
|
|
63
|
+
vars: dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Config:
|
|
68
|
+
vars: dict[str, Any]
|
|
69
|
+
remotes: dict[str, dict[str, RemoteConfig]] # alias -> remote_name -> cfg
|
|
70
|
+
root_dir: Path
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def load_workspace_config(path: Path) -> WorkspaceConfig:
|
|
74
|
+
"""Read a .ow/config TOML file from an individual workspace."""
|
|
75
|
+
with open(path, "rb") as f:
|
|
76
|
+
data = tomllib.load(f)
|
|
77
|
+
|
|
78
|
+
repos = {}
|
|
79
|
+
for alias, spec_str in data.get("repos", {}).items():
|
|
80
|
+
repos[alias] = parse_branch_spec(spec_str)
|
|
81
|
+
|
|
82
|
+
templates = data.get("templates")
|
|
83
|
+
if templates is None:
|
|
84
|
+
raise ValueError(f"Workspace config '{path}' missing required 'templates' field")
|
|
85
|
+
if not isinstance(templates, list):
|
|
86
|
+
raise ValueError(f"Workspace config '{path}' 'templates' must be a list")
|
|
87
|
+
|
|
88
|
+
return WorkspaceConfig(
|
|
89
|
+
repos=repos,
|
|
90
|
+
templates=templates,
|
|
91
|
+
vars=data.get("vars", {}),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def write_workspace_config(path: Path, ws: WorkspaceConfig) -> None:
|
|
96
|
+
"""Write a .ow/config TOML file for an individual workspace."""
|
|
97
|
+
data: dict[str, Any] = {
|
|
98
|
+
"templates": ws.templates,
|
|
99
|
+
"repos": {alias: spec.to_spec_str() for alias, spec in ws.repos.items()},
|
|
100
|
+
}
|
|
101
|
+
if ws.vars:
|
|
102
|
+
data["vars"] = ws.vars
|
|
103
|
+
|
|
104
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
with open(path, "wb") as f:
|
|
106
|
+
tomli_w.dump(data, f)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def load_config(path: Path) -> Config:
|
|
110
|
+
with open(path, "rb") as f:
|
|
111
|
+
data = tomllib.load(f)
|
|
112
|
+
|
|
113
|
+
vars = data.get("vars", {})
|
|
114
|
+
|
|
115
|
+
remotes: dict[str, dict[str, RemoteConfig]] = {}
|
|
116
|
+
for alias, remote_dict in data.get("remotes", {}).items():
|
|
117
|
+
remotes[alias] = {}
|
|
118
|
+
for remote_name, remote_cfg in remote_dict.items():
|
|
119
|
+
remotes[alias][remote_name] = RemoteConfig(
|
|
120
|
+
url=remote_cfg["url"],
|
|
121
|
+
pushurl=remote_cfg.get("pushurl"),
|
|
122
|
+
fetch=remote_cfg.get("fetch"),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return Config(
|
|
126
|
+
vars=vars,
|
|
127
|
+
remotes=remotes,
|
|
128
|
+
root_dir=path.parent,
|
|
129
|
+
)
|