odooflow-cli 0.2.0__tar.gz → 0.3.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.
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/PKG-INFO +83 -6
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/README.md +82 -5
- odooflow_cli-0.3.0/odooflow/__init__.py +1 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow/cli.py +16 -4
- odooflow_cli-0.3.0/odooflow/commands/clone_module.py +273 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow/commands/config.py +38 -10
- odooflow_cli-0.3.0/odooflow/commands/gitlab.py +84 -0
- odooflow_cli-0.3.0/odooflow/commands/init_module_env.py +80 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow/commands/push.py +21 -4
- odooflow_cli-0.3.0/odooflow/commands/remote.py +238 -0
- odooflow_cli-0.3.0/odooflow/commands/server.py +559 -0
- odooflow_cli-0.3.0/odooflow/commands/setup.py +131 -0
- odooflow_cli-0.3.0/odooflow/config_manager.py +198 -0
- odooflow_cli-0.3.0/odooflow/errors.py +308 -0
- odooflow_cli-0.3.0/odooflow/utils/env.py +80 -0
- odooflow_cli-0.3.0/odooflow/utils/server_profile.py +319 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow_cli.egg-info/PKG-INFO +83 -6
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow_cli.egg-info/SOURCES.txt +9 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow_cli.egg-info/top_level.txt +1 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/pyproject.toml +1 -1
- odooflow_cli-0.3.0/tests/test_clone_module.py +219 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/tests/test_commands_remote.py +4 -2
- odooflow_cli-0.3.0/tests/test_commands_server.py +504 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/tests/test_config_manager.py +63 -15
- odooflow_cli-0.3.0/tests/test_gitlab.py +110 -0
- odooflow_cli-0.3.0/tests/test_server_profile.py +339 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/tests/test_utils_env.py +14 -14
- odooflow_cli-0.2.0/odooflow/__init__.py +0 -1
- odooflow_cli-0.2.0/odooflow/commands/clone_module.py +0 -160
- odooflow_cli-0.2.0/odooflow/commands/init_module_env.py +0 -37
- odooflow_cli-0.2.0/odooflow/commands/remote.py +0 -133
- odooflow_cli-0.2.0/odooflow/config_manager.py +0 -56
- odooflow_cli-0.2.0/odooflow/utils/env.py +0 -40
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/LICENSE +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow/commands/__init__.py +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow/commands/keygen.py +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow/commands/sync_env.py +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow/utils/ssh.py +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow_cli.egg-info/dependency_links.txt +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow_cli.egg-info/entry_points.txt +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/odooflow_cli.egg-info/requires.txt +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/setup.cfg +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/tests/__init__.py +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/tests/test_commands_config.py +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/tests/test_commands_init_module_env.py +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/tests/test_commands_keygen.py +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/tests/test_commands_sync_env.py +0 -0
- {odooflow_cli-0.2.0 → odooflow_cli-0.3.0}/tests/test_utils_ssh.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: odooflow-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: OdooFlow CLI - streamline your Odoo development workflow
|
|
5
5
|
Author: Mohammad A. Hamdan
|
|
6
6
|
License: MIT
|
|
@@ -42,13 +42,14 @@ Dynamic: license-file
|
|
|
42
42
|
- Smart skip of Odoo core modules
|
|
43
43
|
- Branch selection for cloning
|
|
44
44
|
- Post-push command execution on the remote server
|
|
45
|
+
- **Named server profiles** (staging/QA/prod) with first-class `odooflow server list|add|show|use|remove|test`
|
|
45
46
|
- Built-in SSH key generation
|
|
46
47
|
- Helpful and colorful CLI output
|
|
47
48
|
- Built using [Typer](https://typer.tiangolo.com/) and Python 3.7+
|
|
48
49
|
|
|
49
50
|
---
|
|
50
51
|
|
|
51
|
-
## 📦 Installation
|
|
52
|
+
## 📦 Installation & first-run setup
|
|
52
53
|
|
|
53
54
|
```bash
|
|
54
55
|
git clone https://github.com/anomalyco/odooflow-cli.git
|
|
@@ -68,6 +69,29 @@ Install from PyPI (once published):
|
|
|
68
69
|
pip install odooflow-cli
|
|
69
70
|
```
|
|
70
71
|
|
|
72
|
+
### First-run wizard
|
|
73
|
+
|
|
74
|
+
After installing, configure odooflow with your GitLab access token:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
odooflow setup
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The wizard writes `~/.odooflowrc` (with `chmod 600` permissions), prompting for:
|
|
81
|
+
|
|
82
|
+
1. **GitLab access token** — kept private in the rc file; typed input is masked.
|
|
83
|
+
2. **GitLab URL** — defaults to the bundled one, override for self-hosted.
|
|
84
|
+
3. **Core modules** — comma-separated list, used to skip framework deps.
|
|
85
|
+
|
|
86
|
+
If you don't have a token yet, create one at *GitLab → Preferences → Access Tokens* with scopes `api`, `read_api`, and `write_repository`.
|
|
87
|
+
|
|
88
|
+
Prefer environment variables? You can skip the rc entirely:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
export ODOOFLOW_ACCESS_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
|
|
92
|
+
odooflow clone <your-module-url>
|
|
93
|
+
```
|
|
94
|
+
|
|
71
95
|
---
|
|
72
96
|
|
|
73
97
|
## 🛠️ Usage
|
|
@@ -80,11 +104,13 @@ odooflow --help
|
|
|
80
104
|
|
|
81
105
|
### Available Commands:
|
|
82
106
|
|
|
107
|
+
- **`setup`**: Interactive wizard for first-run configuration (`~/.odooflowrc`).
|
|
83
108
|
- **`init`**: Initialize the Odoo module environment file and sync metadata with manifest
|
|
84
109
|
- **`sync-env`**: Sync the environment file from manifest
|
|
85
110
|
- **`config`**: Update or show OdooFlow CLI configuration
|
|
86
111
|
- **`clone`**: Clone a module and its dependencies from a git repository
|
|
87
112
|
- **`remote`**: Manage remote connections for Git and deployment server
|
|
113
|
+
- **`server`**: Manage named server profiles (staging/QA/prod) — `list`, `add`, `show`, `use`, `remove`, `test`
|
|
88
114
|
- **`ssh-keygen`**: Generate a secure SSH key pair
|
|
89
115
|
- **`push`**: Push the current Git branch and upload the project to the test server
|
|
90
116
|
|
|
@@ -98,10 +124,23 @@ odooflow --help
|
|
|
98
124
|
|
|
99
125
|
### Push Command Options:
|
|
100
126
|
|
|
101
|
-
| Flag | Description
|
|
102
|
-
|
|
103
|
-
| `--
|
|
104
|
-
| `--
|
|
127
|
+
| Flag | Description |
|
|
128
|
+
|-----------------|----------------------------------------------------------------------------------------------------------|
|
|
129
|
+
| `--server`/`-s` | Named server profile from `odooflow server list` (defaults to the configured default). |
|
|
130
|
+
| `--remote-only` | Skip Git push and only upload to server |
|
|
131
|
+
| `--exec` | Custom shell command to execute on the server after pushing |
|
|
132
|
+
|
|
133
|
+
### Server Profile Commands:
|
|
134
|
+
|
|
135
|
+
| Command | What it does |
|
|
136
|
+
|--------------------------------------|-------------------------------------------------------|
|
|
137
|
+
| `odooflow server list` | Tabular view of every configured profile. |
|
|
138
|
+
| `odooflow server list --json` | Machine-readable output (passwords omitted). |
|
|
139
|
+
| `odooflow server add <name>` | Interactive wizard (or `--host`, `--user`, `--key-path` for non-interactive). Validates inputs and tests SSH on save. |
|
|
140
|
+
| `odooflow server show [<name>]` | Show fields of a profile. Default = the current default. Passwords are masked unless `--reveal-password`. |
|
|
141
|
+
| `odooflow server use <name>` | Set the default profile used by `odooflow push`. |
|
|
142
|
+
| `odooflow server remove <name>` | Delete a profile (the default reverts to another if any are left). |
|
|
143
|
+
| `odooflow server test [<name>]` | Verify TCP reachability, SSH auth, and directory existence without uploading anything. |
|
|
105
144
|
|
|
106
145
|
### 🔍 Examples:
|
|
107
146
|
|
|
@@ -147,6 +186,42 @@ Skip Git push, only upload to server:
|
|
|
147
186
|
odooflow push --remote-only
|
|
148
187
|
```
|
|
149
188
|
|
|
189
|
+
Push to a specific server (when you have several profiles):
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
odooflow push --server staging
|
|
193
|
+
odooflow push --server prod --remote-only --exec 'sudo systemctl restart odoo-prod'
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 📡 Server profiles — a faster flow for `staging` / `qa` / `prod`
|
|
197
|
+
|
|
198
|
+
Add as many named profiles as you want. The first one you create is the default for `odooflow push`:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Interactive wizard — validates every field and tests SSH before saving.
|
|
202
|
+
odooflow server add staging
|
|
203
|
+
|
|
204
|
+
# Non-interactive / scriptable:
|
|
205
|
+
odooflow server add qa \
|
|
206
|
+
--host 10.0.0.5 --port 22 --user deploy \
|
|
207
|
+
--directory /opt/odoo/qa --key-path ~/.ssh/odooflow_rsa
|
|
208
|
+
|
|
209
|
+
# Review what you have:
|
|
210
|
+
odooflow server list
|
|
211
|
+
odooflow server show staging # password is masked; --reveal-password to see it
|
|
212
|
+
|
|
213
|
+
# Switch the default:
|
|
214
|
+
odooflow server use prod
|
|
215
|
+
|
|
216
|
+
# Verify connectivity without uploading:
|
|
217
|
+
odooflow server test staging
|
|
218
|
+
|
|
219
|
+
# Reorder / remove:
|
|
220
|
+
odooflow server remove qa
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Existing single-server configs are auto-migrated into a `default` profile on first `odooflow server add`, so nothing you've already configured is lost.
|
|
224
|
+
|
|
150
225
|
---
|
|
151
226
|
|
|
152
227
|
## 📁 Project Structure
|
|
@@ -165,9 +240,11 @@ odooflow/
|
|
|
165
240
|
│ │ ├── keygen.py
|
|
166
241
|
│ │ ├── push.py
|
|
167
242
|
│ │ ├── remote.py
|
|
243
|
+
│ │ ├── server.py
|
|
168
244
|
│ │ └── sync_env.py
|
|
169
245
|
│ └── utils/
|
|
170
246
|
│ ├── env.py
|
|
247
|
+
│ ├── server_profile.py
|
|
171
248
|
│ └── ssh.py
|
|
172
249
|
├── tests/
|
|
173
250
|
├── README.md
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
- Smart skip of Odoo core modules
|
|
10
10
|
- Branch selection for cloning
|
|
11
11
|
- Post-push command execution on the remote server
|
|
12
|
+
- **Named server profiles** (staging/QA/prod) with first-class `odooflow server list|add|show|use|remove|test`
|
|
12
13
|
- Built-in SSH key generation
|
|
13
14
|
- Helpful and colorful CLI output
|
|
14
15
|
- Built using [Typer](https://typer.tiangolo.com/) and Python 3.7+
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
18
|
-
## 📦 Installation
|
|
19
|
+
## 📦 Installation & first-run setup
|
|
19
20
|
|
|
20
21
|
```bash
|
|
21
22
|
git clone https://github.com/anomalyco/odooflow-cli.git
|
|
@@ -35,6 +36,29 @@ Install from PyPI (once published):
|
|
|
35
36
|
pip install odooflow-cli
|
|
36
37
|
```
|
|
37
38
|
|
|
39
|
+
### First-run wizard
|
|
40
|
+
|
|
41
|
+
After installing, configure odooflow with your GitLab access token:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
odooflow setup
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The wizard writes `~/.odooflowrc` (with `chmod 600` permissions), prompting for:
|
|
48
|
+
|
|
49
|
+
1. **GitLab access token** — kept private in the rc file; typed input is masked.
|
|
50
|
+
2. **GitLab URL** — defaults to the bundled one, override for self-hosted.
|
|
51
|
+
3. **Core modules** — comma-separated list, used to skip framework deps.
|
|
52
|
+
|
|
53
|
+
If you don't have a token yet, create one at *GitLab → Preferences → Access Tokens* with scopes `api`, `read_api`, and `write_repository`.
|
|
54
|
+
|
|
55
|
+
Prefer environment variables? You can skip the rc entirely:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
export ODOOFLOW_ACCESS_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
|
|
59
|
+
odooflow clone <your-module-url>
|
|
60
|
+
```
|
|
61
|
+
|
|
38
62
|
---
|
|
39
63
|
|
|
40
64
|
## 🛠️ Usage
|
|
@@ -47,11 +71,13 @@ odooflow --help
|
|
|
47
71
|
|
|
48
72
|
### Available Commands:
|
|
49
73
|
|
|
74
|
+
- **`setup`**: Interactive wizard for first-run configuration (`~/.odooflowrc`).
|
|
50
75
|
- **`init`**: Initialize the Odoo module environment file and sync metadata with manifest
|
|
51
76
|
- **`sync-env`**: Sync the environment file from manifest
|
|
52
77
|
- **`config`**: Update or show OdooFlow CLI configuration
|
|
53
78
|
- **`clone`**: Clone a module and its dependencies from a git repository
|
|
54
79
|
- **`remote`**: Manage remote connections for Git and deployment server
|
|
80
|
+
- **`server`**: Manage named server profiles (staging/QA/prod) — `list`, `add`, `show`, `use`, `remove`, `test`
|
|
55
81
|
- **`ssh-keygen`**: Generate a secure SSH key pair
|
|
56
82
|
- **`push`**: Push the current Git branch and upload the project to the test server
|
|
57
83
|
|
|
@@ -65,10 +91,23 @@ odooflow --help
|
|
|
65
91
|
|
|
66
92
|
### Push Command Options:
|
|
67
93
|
|
|
68
|
-
| Flag | Description
|
|
69
|
-
|
|
70
|
-
| `--
|
|
71
|
-
| `--
|
|
94
|
+
| Flag | Description |
|
|
95
|
+
|-----------------|----------------------------------------------------------------------------------------------------------|
|
|
96
|
+
| `--server`/`-s` | Named server profile from `odooflow server list` (defaults to the configured default). |
|
|
97
|
+
| `--remote-only` | Skip Git push and only upload to server |
|
|
98
|
+
| `--exec` | Custom shell command to execute on the server after pushing |
|
|
99
|
+
|
|
100
|
+
### Server Profile Commands:
|
|
101
|
+
|
|
102
|
+
| Command | What it does |
|
|
103
|
+
|--------------------------------------|-------------------------------------------------------|
|
|
104
|
+
| `odooflow server list` | Tabular view of every configured profile. |
|
|
105
|
+
| `odooflow server list --json` | Machine-readable output (passwords omitted). |
|
|
106
|
+
| `odooflow server add <name>` | Interactive wizard (or `--host`, `--user`, `--key-path` for non-interactive). Validates inputs and tests SSH on save. |
|
|
107
|
+
| `odooflow server show [<name>]` | Show fields of a profile. Default = the current default. Passwords are masked unless `--reveal-password`. |
|
|
108
|
+
| `odooflow server use <name>` | Set the default profile used by `odooflow push`. |
|
|
109
|
+
| `odooflow server remove <name>` | Delete a profile (the default reverts to another if any are left). |
|
|
110
|
+
| `odooflow server test [<name>]` | Verify TCP reachability, SSH auth, and directory existence without uploading anything. |
|
|
72
111
|
|
|
73
112
|
### 🔍 Examples:
|
|
74
113
|
|
|
@@ -114,6 +153,42 @@ Skip Git push, only upload to server:
|
|
|
114
153
|
odooflow push --remote-only
|
|
115
154
|
```
|
|
116
155
|
|
|
156
|
+
Push to a specific server (when you have several profiles):
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
odooflow push --server staging
|
|
160
|
+
odooflow push --server prod --remote-only --exec 'sudo systemctl restart odoo-prod'
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 📡 Server profiles — a faster flow for `staging` / `qa` / `prod`
|
|
164
|
+
|
|
165
|
+
Add as many named profiles as you want. The first one you create is the default for `odooflow push`:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Interactive wizard — validates every field and tests SSH before saving.
|
|
169
|
+
odooflow server add staging
|
|
170
|
+
|
|
171
|
+
# Non-interactive / scriptable:
|
|
172
|
+
odooflow server add qa \
|
|
173
|
+
--host 10.0.0.5 --port 22 --user deploy \
|
|
174
|
+
--directory /opt/odoo/qa --key-path ~/.ssh/odooflow_rsa
|
|
175
|
+
|
|
176
|
+
# Review what you have:
|
|
177
|
+
odooflow server list
|
|
178
|
+
odooflow server show staging # password is masked; --reveal-password to see it
|
|
179
|
+
|
|
180
|
+
# Switch the default:
|
|
181
|
+
odooflow server use prod
|
|
182
|
+
|
|
183
|
+
# Verify connectivity without uploading:
|
|
184
|
+
odooflow server test staging
|
|
185
|
+
|
|
186
|
+
# Reorder / remove:
|
|
187
|
+
odooflow server remove qa
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Existing single-server configs are auto-migrated into a `default` profile on first `odooflow server add`, so nothing you've already configured is lost.
|
|
191
|
+
|
|
117
192
|
---
|
|
118
193
|
|
|
119
194
|
## 📁 Project Structure
|
|
@@ -132,9 +207,11 @@ odooflow/
|
|
|
132
207
|
│ │ ├── keygen.py
|
|
133
208
|
│ │ ├── push.py
|
|
134
209
|
│ │ ├── remote.py
|
|
210
|
+
│ │ ├── server.py
|
|
135
211
|
│ │ └── sync_env.py
|
|
136
212
|
│ └── utils/
|
|
137
213
|
│ ├── env.py
|
|
214
|
+
│ ├── server_profile.py
|
|
138
215
|
│ └── ssh.py
|
|
139
216
|
├── tests/
|
|
140
217
|
├── README.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -8,8 +8,16 @@ from odooflow.commands.clone_module import clone_module_command
|
|
|
8
8
|
from odooflow.commands.remote import remote as remote_command
|
|
9
9
|
from odooflow.commands.keygen import generate_ssh_key as keygen_command
|
|
10
10
|
from odooflow.commands.push import push_command
|
|
11
|
+
from odooflow.commands.setup import setup as setup_command
|
|
12
|
+
from odooflow.commands.server import app as server_app
|
|
11
13
|
|
|
12
14
|
app = typer.Typer(help="OdooFlow CLI — streamline your Odoo development workflow.")
|
|
15
|
+
app.add_typer(server_app, name="server")
|
|
16
|
+
|
|
17
|
+
@app.command(name="setup")
|
|
18
|
+
def setup_cmd():
|
|
19
|
+
"""Interactive wizard: write ~/.odooflowrc with token, GitLab URL, core modules."""
|
|
20
|
+
setup_command()
|
|
13
21
|
|
|
14
22
|
@app.command(name="init")
|
|
15
23
|
def init_manifest(
|
|
@@ -51,12 +59,15 @@ def config(
|
|
|
51
59
|
def clone_command(
|
|
52
60
|
repo_url: str = typer.Argument(..., help="HTTP URL of the module repository."),
|
|
53
61
|
branch: Optional[str] = typer.Option(None, "--branch", '-b', help="Branch to clone"),
|
|
54
|
-
depth: int = typer.Option(1, "--depth", "-d", help="Max dependency depth to clone. 1
|
|
62
|
+
depth: int = typer.Option(1, "--depth", "-d", help="Max dependency depth to clone. 1 = target only, 2 = target + immediate deps, etc."),
|
|
63
|
+
workers: int = typer.Option(4, "--workers", "-w", help="Max concurrent clones (1-8)."),
|
|
55
64
|
):
|
|
56
65
|
"""
|
|
57
|
-
Clone a module and its dependencies from a
|
|
66
|
+
Clone a module and (optionally) its dependencies from a Git repository.
|
|
67
|
+
|
|
68
|
+
Run `odooflow setup` first if you have not configured an access token yet.
|
|
58
69
|
"""
|
|
59
|
-
clone_module_command(repo_url, branch, depth)
|
|
70
|
+
clone_module_command(repo_url, branch, depth, workers)
|
|
60
71
|
|
|
61
72
|
|
|
62
73
|
@app.command()
|
|
@@ -88,13 +99,14 @@ def generate_ssh_key(
|
|
|
88
99
|
|
|
89
100
|
@app.command()
|
|
90
101
|
def push(
|
|
102
|
+
server: Optional[str] = typer.Option(None, "--server", "-s", help="Named server profile from `odooflow server list`."),
|
|
91
103
|
remote_only: bool = typer.Option(False, "--remote-only", help="Skip Git push and only upload to server"),
|
|
92
104
|
exec_cmd: Optional[str] = typer.Option(None, "--exec", help="Custom shell command to execute on the server after pushing"),
|
|
93
105
|
):
|
|
94
106
|
"""
|
|
95
107
|
Push the current Git branch and upload the project to the test server.
|
|
96
108
|
"""
|
|
97
|
-
push_command(remote_only=remote_only, exec_cmd=exec_cmd)
|
|
109
|
+
push_command(server_name=server, remote_only=remote_only, exec_cmd=exec_cmd)
|
|
98
110
|
|
|
99
111
|
|
|
100
112
|
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import typer
|
|
3
|
+
import requests
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
|
|
9
|
+
from urllib.parse import urlparse, urlunparse
|
|
10
|
+
from git import Repo, GitCommandError
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from odooflow import errors
|
|
14
|
+
from odooflow.commands.gitlab import get_default_branch
|
|
15
|
+
from odooflow.config_manager import (
|
|
16
|
+
get_access_token,
|
|
17
|
+
get_core_modules_from_config,
|
|
18
|
+
load_config,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_project_url_from_gitlab(module_name: str, base_url: Optional[str] = None) -> Optional[str]:
|
|
23
|
+
"""Search GitLab for a project by name and return its HTTPS URL."""
|
|
24
|
+
if base_url is None:
|
|
25
|
+
config = load_config(strict=False)
|
|
26
|
+
base_url = config.get("gitlab_url", "https://gitlab.ebtech-solution.com")
|
|
27
|
+
|
|
28
|
+
api_url = f"{base_url}/api/v4/projects"
|
|
29
|
+
headers = {"Accept": "application/json"}
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
token = get_access_token()
|
|
33
|
+
except errors.AccessTokenMissingError:
|
|
34
|
+
errors.access_token_missing_rc_fallback()
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
params = {"search": module_name, "simple": "true", "per_page": 100, "access_token": token}
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
typer.secho(f" 🔍 Looking up '{module_name}' in GitLab…", fg="cyan")
|
|
41
|
+
response = requests.get(api_url, headers=headers, params=params, timeout=15)
|
|
42
|
+
response.raise_for_status()
|
|
43
|
+
|
|
44
|
+
for project in response.json():
|
|
45
|
+
if project.get("name") == module_name or project.get("path") == module_name:
|
|
46
|
+
typer.secho(f" ✓ Resolved '{module_name}'", fg="green")
|
|
47
|
+
return project["http_url_to_repo"]
|
|
48
|
+
|
|
49
|
+
errors.dependency_unresolved(module_name)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
except requests.RequestException as e:
|
|
53
|
+
errors.gitlab_unreachable(base_url, str(e))
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def inject_token_into_url(url: str, token: str) -> str:
|
|
58
|
+
"""Embed a GitLab PAT into a clone URL as `oauth2:<token>@host`."""
|
|
59
|
+
parsed = urlparse(url)
|
|
60
|
+
if not parsed.netloc:
|
|
61
|
+
raise ValueError(f"Invalid URL: {url}")
|
|
62
|
+
netloc = f"oauth2:{token}@{parsed.netloc}"
|
|
63
|
+
return urlunparse(parsed._replace(netloc=netloc, scheme="https"))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def extract_module_name_from_url(url: str) -> str:
|
|
67
|
+
return url.rstrip("/").split("/")[-1].removesuffix(".git")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def safe_eval_manifest(content: str) -> dict:
|
|
71
|
+
try:
|
|
72
|
+
return ast.literal_eval(content)
|
|
73
|
+
except (SyntaxError, ValueError) as e:
|
|
74
|
+
errors._emit(
|
|
75
|
+
"Manifest could not be parsed.",
|
|
76
|
+
[
|
|
77
|
+
f" Python error: {e}",
|
|
78
|
+
"",
|
|
79
|
+
" Treat the module as having no dependencies; fix the manifest",
|
|
80
|
+
" in the cloned copy before re-running with --depth > 1.",
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_access_token_safe() -> bool:
|
|
87
|
+
try:
|
|
88
|
+
get_access_token()
|
|
89
|
+
return True
|
|
90
|
+
except errors.AccessTokenMissingError:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _resolve_branch(repo_url: str, requested: Optional[str]) -> Optional[str]:
|
|
95
|
+
"""
|
|
96
|
+
Decide which branch to pass to `git clone -b <branch>`.
|
|
97
|
+
|
|
98
|
+
Resolution order:
|
|
99
|
+
1. Caller-supplied branch (CLI `--branch` or per-dependency value) wins.
|
|
100
|
+
2. GitLab API `default_branch` for the project's URL.
|
|
101
|
+
3. None — caller passes no `-b` flag and git uses its own default.
|
|
102
|
+
"""
|
|
103
|
+
if requested:
|
|
104
|
+
return requested
|
|
105
|
+
try:
|
|
106
|
+
default = get_default_branch(repo_url)
|
|
107
|
+
except Exception:
|
|
108
|
+
default = None
|
|
109
|
+
return default or None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def clone_repo(url: str, target_dir: Path, branch: str = None) -> bool:
|
|
113
|
+
"""
|
|
114
|
+
Clone `url` into `target_dir`.
|
|
115
|
+
|
|
116
|
+
`branch` precedence (handled by `_resolve_branch`):
|
|
117
|
+
CLI `--branch X` > GitLab `default_branch` > git's own default.
|
|
118
|
+
"""
|
|
119
|
+
if target_dir.exists():
|
|
120
|
+
typer.secho(f" ⚠ '{target_dir.name}' already exists, skipping.", fg="yellow")
|
|
121
|
+
return True
|
|
122
|
+
try:
|
|
123
|
+
access_token = get_access_token()
|
|
124
|
+
url_with_token = inject_token_into_url(url, access_token)
|
|
125
|
+
chosen = _resolve_branch(url, branch)
|
|
126
|
+
branch_display = f"branch '{chosen}'" if chosen else "default branch"
|
|
127
|
+
typer.secho(f" ⇣ Cloning '{target_dir.name}' ({branch_display})…", fg="cyan")
|
|
128
|
+
if chosen:
|
|
129
|
+
Repo.clone_from(url_with_token, target_dir, branch=chosen)
|
|
130
|
+
else:
|
|
131
|
+
Repo.clone_from(url_with_token, target_dir)
|
|
132
|
+
typer.secho(f" ✓ Cloned '{target_dir.name}'", fg="green")
|
|
133
|
+
return True
|
|
134
|
+
except GitCommandError as e:
|
|
135
|
+
stderr = (getattr(e, "stderr", "") or "").strip()
|
|
136
|
+
reason = f"Git error: {e}"
|
|
137
|
+
# Heuristic: if git complained the branch isn't upstream, surface the real cause
|
|
138
|
+
if "Remote branch" in stderr and "not found" in stderr:
|
|
139
|
+
reason = (
|
|
140
|
+
f"The default branch for this repo is not 'main'. "
|
|
141
|
+
f"git said: {stderr.splitlines()[-1]}"
|
|
142
|
+
)
|
|
143
|
+
errors.clone_failed(target_dir.name, reason)
|
|
144
|
+
return False
|
|
145
|
+
except errors.AccessTokenMissingError:
|
|
146
|
+
errors.access_token_missing_rc_fallback()
|
|
147
|
+
return False
|
|
148
|
+
except Exception as e:
|
|
149
|
+
errors.clone_failed(target_dir.name, f"Unexpected error: {e}")
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def clone_module_command(
|
|
154
|
+
url: str = typer.Option(..., "--url", help="Full HTTP URL of the module repo."),
|
|
155
|
+
branch: Optional[str] = None,
|
|
156
|
+
depth: int = typer.Option(1, "--depth", "-d", help="Max dependency depth to clone. 1 clones only the target module, 2 clones target + immediate dependencies, etc."),
|
|
157
|
+
workers: int = typer.Option(4, "--workers", "-w", help="Max concurrent clones (1-8)."),
|
|
158
|
+
):
|
|
159
|
+
"""Clone a module and (optionally) its dependencies into the current directory."""
|
|
160
|
+
try:
|
|
161
|
+
core_modules = get_core_modules_from_config()
|
|
162
|
+
except errors.ConfigError as e:
|
|
163
|
+
errors._safe_exit(e)
|
|
164
|
+
|
|
165
|
+
if not get_access_token_safe():
|
|
166
|
+
typer.secho("")
|
|
167
|
+
typer.secho("┌─ odooflow setup needed", fg="cyan", bold=True)
|
|
168
|
+
typer.secho(
|
|
169
|
+
"│ No GitLab access token found. Run `odooflow setup` to create one",
|
|
170
|
+
fg="cyan",
|
|
171
|
+
)
|
|
172
|
+
typer.secho(
|
|
173
|
+
"│ interactively, or set ODOOFLOW_ACCESS_TOKEN in your shell.",
|
|
174
|
+
fg="cyan",
|
|
175
|
+
)
|
|
176
|
+
typer.secho("")
|
|
177
|
+
typer.secho(
|
|
178
|
+
" Example:\n"
|
|
179
|
+
" export ODOOFLOW_ACCESS_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx",
|
|
180
|
+
fg="cyan",
|
|
181
|
+
)
|
|
182
|
+
typer.secho("")
|
|
183
|
+
raise typer.Exit(code=1)
|
|
184
|
+
|
|
185
|
+
typer.secho("")
|
|
186
|
+
typer.secho("┌─ odooflow clone", fg="cyan", bold=True)
|
|
187
|
+
typer.secho(
|
|
188
|
+
f"│ Depth: {depth} Workers: {max(1, min(workers, 8))}",
|
|
189
|
+
fg="cyan",
|
|
190
|
+
)
|
|
191
|
+
typer.secho("")
|
|
192
|
+
|
|
193
|
+
visited = set()
|
|
194
|
+
fail_count = 0
|
|
195
|
+
lock = threading.Lock()
|
|
196
|
+
|
|
197
|
+
def clone_recursive(module_url: str, current_branch: Optional[str], current_depth: int):
|
|
198
|
+
nonlocal fail_count
|
|
199
|
+
module_name = extract_module_name_from_url(module_url)
|
|
200
|
+
|
|
201
|
+
with lock:
|
|
202
|
+
if module_name in visited:
|
|
203
|
+
typer.secho(f" ↻ Already processed '{module_name}'.", fg="yellow")
|
|
204
|
+
return False
|
|
205
|
+
visited.add(module_name)
|
|
206
|
+
|
|
207
|
+
target_path = Path.cwd() / module_name
|
|
208
|
+
|
|
209
|
+
if not clone_repo(module_url, target_path, current_branch):
|
|
210
|
+
typer.secho(f" ✗ Skipping dependencies of '{module_name}'.", fg="red")
|
|
211
|
+
with lock:
|
|
212
|
+
fail_count += 1
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
if current_depth <= 0:
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
manifest_path = target_path / "__manifest__.py"
|
|
219
|
+
if not manifest_path.exists():
|
|
220
|
+
typer.secho(f" · No manifest in '{module_name}'.", fg="yellow")
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
manifest_data = safe_eval_manifest(manifest_path.read_text())
|
|
224
|
+
dependencies = manifest_data.get("depends", [])
|
|
225
|
+
|
|
226
|
+
if not dependencies:
|
|
227
|
+
typer.secho(f" · '{module_name}' has no dependencies.", fg="cyan")
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
candidate_deps = [dep for dep in dependencies if dep not in core_modules]
|
|
231
|
+
if not candidate_deps:
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
next_depth = current_depth - 1
|
|
235
|
+
|
|
236
|
+
def _resolve_and_run(dep_name: str):
|
|
237
|
+
nonlocal fail_count
|
|
238
|
+
dep_url = get_project_url_from_gitlab(module_name=dep_name)
|
|
239
|
+
if not dep_url:
|
|
240
|
+
with lock:
|
|
241
|
+
fail_count += 1
|
|
242
|
+
return
|
|
243
|
+
clone_recursive(dep_url, current_branch, next_depth)
|
|
244
|
+
|
|
245
|
+
typer.secho(
|
|
246
|
+
f" ⇢ Resolving {len(candidate_deps)} dependency(ies) of '{module_name}' in parallel…",
|
|
247
|
+
fg="cyan",
|
|
248
|
+
)
|
|
249
|
+
pool_size = max(1, min(workers, 8))
|
|
250
|
+
with ThreadPoolExecutor(max_workers=pool_size) as executor:
|
|
251
|
+
futures = [executor.submit(_resolve_and_run, dep) for dep in candidate_deps]
|
|
252
|
+
for f in futures:
|
|
253
|
+
f.result()
|
|
254
|
+
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
clone_recursive(url, branch, depth)
|
|
258
|
+
|
|
259
|
+
typer.secho("")
|
|
260
|
+
typer.secho("└─ odooflow clone finished", fg="cyan", bold=True)
|
|
261
|
+
if fail_count > 0:
|
|
262
|
+
typer.secho(
|
|
263
|
+
f" ✗ {fail_count} module(s) failed to clone. See messages above.",
|
|
264
|
+
fg="red",
|
|
265
|
+
bold=True,
|
|
266
|
+
)
|
|
267
|
+
raise typer.Exit(code=1)
|
|
268
|
+
typer.secho(
|
|
269
|
+
f" ✓ All {len(visited)} module(s) processed without errors.",
|
|
270
|
+
fg="green",
|
|
271
|
+
bold=True,
|
|
272
|
+
)
|
|
273
|
+
typer.secho("")
|