odoo-dev 1.2.1__tar.gz → 1.2.3__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_dev-1.2.3/.release-please-manifest.json +3 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/CHANGELOG.md +17 -0
- odoo_dev-1.2.1/README.md → odoo_dev-1.2.3/PKG-INFO +28 -2
- odoo_dev-1.2.1/PKG-INFO → odoo_dev-1.2.3/README.md +14 -16
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/pyproject.toml +4 -4
- odoo_dev-1.2.3/src/odoo_dev/__init__.py +3 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/commands/vendor.py +32 -3
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/vendor/materialize.py +15 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/vendor/migrate.py +119 -8
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/vendor/verify.py +49 -1
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_vendor_materialize.py +31 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_vendor_migrate.py +85 -2
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_vendor_verify.py +67 -2
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/uv.lock +1 -1
- odoo_dev-1.2.1/.release-please-manifest.json +0 -3
- odoo_dev-1.2.1/src/odoo_dev/__init__.py +0 -3
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/.github/workflows/release-please.yml +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/.gitignore +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/.gitmodules +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/.python-version +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/release-please-config.json +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/cli.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/commands/__init__.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/commands/bump.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/commands/db.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/commands/docker.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/commands/run.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/commands/setup.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/config.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/preflight.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/templates/__init__.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/templates/docker/Dockerfile +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/templates/docker/__init__.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/templates/docker/docker-entrypoint.sh +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/templates/vscode/__init__.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/templates/vscode/launch.json +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/templates/vscode/settings.json +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/templates/vscode/tasks.json +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/utils/__init__.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/utils/console.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/utils/manifest.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/vendor/__init__.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/vendor/develop.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/vendor/edit.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/vendor/lock.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/vendor/sources.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/vendor/sync.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/src/odoo_dev/vendor/update.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/__init__.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/fixtures/__init__.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_bump.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_cli.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_config.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_db.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_db_config.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_drop_database.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_integration.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_preflight.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_run.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_setup.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_setup_db.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_vendor_develop.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_vendor_edit.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_vendor_lock.py +0 -0
- {odoo_dev-1.2.1 → odoo_dev-1.2.3}/tests/test_vendor_update.py +0 -0
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.3](https://github.com/bemade/odoo-dev/compare/v1.2.2...v1.2.3) (2026-09-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* point project URLs at the GitHub source, not the retired GitLab ([61d7348](https://github.com/bemade/odoo-dev/commit/61d734858e50fb8080b508959b9566b4b3134ba0))
|
|
9
|
+
|
|
10
|
+
## [1.2.2](https://github.com/bemade/odoo-dev/compare/v1.2.1...v1.2.2) (2026-08-14)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **vendor:** exclude python bytecode from the vendored tree comparison ([eab5a0a](https://github.com/bemade/odoo-dev/commit/eab5a0a0779821432724e2be31dcb416e5a60c6b)), closes [#11](https://github.com/bemade/odoo-dev/issues/11)
|
|
16
|
+
* **vendor:** fail when .gitignore would strip files out of vendored/ ([6880616](https://github.com/bemade/odoo-dev/commit/6880616fb3f8d503171f7a6e0f5f6bfe98b667a8)), closes [#13](https://github.com/bemade/odoo-dev/issues/13)
|
|
17
|
+
* **vendor:** pin from the recorded gitlink, refuse uninitialized submodules ([001f86f](https://github.com/bemade/odoo-dev/commit/001f86ff1ae4b89b89681aa8e9878cf2bd9f1f1c)), closes [#12](https://github.com/bemade/odoo-dev/issues/12)
|
|
18
|
+
* **vendor:** three failure modes found in vendoring + CI cycles ([051715a](https://github.com/bemade/odoo-dev/commit/051715a10a17163ebe79cd2fe05a7bbe42328511))
|
|
19
|
+
|
|
3
20
|
## [1.2.1](https://github.com/bemade/odoo-dev/compare/v1.2.0...v1.2.1) (2026-07-14)
|
|
4
21
|
|
|
5
22
|
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: odoo-dev
|
|
3
|
+
Version: 1.2.3
|
|
4
|
+
Summary: Odoo Development Environment Helper
|
|
5
|
+
Project-URL: Homepage, https://github.com/bemade/odoo-dev
|
|
6
|
+
Project-URL: Repository, https://github.com/bemade/odoo-dev
|
|
7
|
+
Project-URL: Issues, https://github.com/bemade/odoo-dev/issues
|
|
8
|
+
License-Expression: LGPL-3.0
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
Requires-Dist: rich>=13.0.0
|
|
12
|
+
Requires-Dist: typer>=0.9.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
1
15
|
# odoo-dev
|
|
2
16
|
|
|
3
17
|
A CLI tool for managing Odoo development environments. Handles local Python setup, Docker containers, database operations, and more.
|
|
@@ -161,8 +175,20 @@ something other than the current pin.
|
|
|
161
175
|
|
|
162
176
|
`vendor check` verifies, per addon: the vendored files byte-match the pinned
|
|
163
177
|
commit; a `version` tag (if set) still resolves to that commit; every manifest
|
|
164
|
-
`external_dependencies['python']` is named in `requirements.txt`;
|
|
165
|
-
name collides between `addons/` and `vendored
|
|
178
|
+
`external_dependencies['python']` is named in `requirements.txt`; no addon
|
|
179
|
+
name collides between `addons/` and `vendored/`; and no file under `vendored/` is
|
|
180
|
+
gitignored. Python bytecode (`__pycache__/`, `*.pyc`) is excluded from the byte
|
|
181
|
+
comparison, so running the test suite doesn't turn the check red.
|
|
182
|
+
|
|
183
|
+
That last assertion catches a trap specific to migration: repo-wide ignore rules
|
|
184
|
+
(`node_modules`, `package.json`, …) are harmless while shared addons live in
|
|
185
|
+
submodules, but they start applying the moment those addons become real files
|
|
186
|
+
under `vendored/` — and `git add` then skips the matching files without a word,
|
|
187
|
+
so the committed tree is incomplete while everything looks fine locally.
|
|
188
|
+
`vendor migrate` appends a `!vendored/**` negation to `.gitignore` when it
|
|
189
|
+
detects the situation. Pins themselves come from the gitlink the branch records,
|
|
190
|
+
not from whatever the submodule happens to be checked out at; migrate refuses to
|
|
191
|
+
run against uninitialized submodules.
|
|
166
192
|
|
|
167
193
|
`vendor update` is the **pull** side: each client owns its pins. For every addon
|
|
168
194
|
that tracks upstream — a `version` (bump to the newest `<addon>/<version>` tag) or
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: odoo-dev
|
|
3
|
-
Version: 1.2.1
|
|
4
|
-
Summary: Odoo Development Environment Helper
|
|
5
|
-
Project-URL: Homepage, https://git.bemade.org/bemade/odoo-dev
|
|
6
|
-
Project-URL: Repository, https://git.bemade.org/bemade/odoo-dev
|
|
7
|
-
Project-URL: Issues, https://git.bemade.org/bemade/odoo-dev/-/issues
|
|
8
|
-
License-Expression: LGPL-3.0
|
|
9
|
-
Requires-Python: >=3.12
|
|
10
|
-
Requires-Dist: pyyaml>=6.0
|
|
11
|
-
Requires-Dist: rich>=13.0.0
|
|
12
|
-
Requires-Dist: typer>=0.9.0
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
|
|
15
1
|
# odoo-dev
|
|
16
2
|
|
|
17
3
|
A CLI tool for managing Odoo development environments. Handles local Python setup, Docker containers, database operations, and more.
|
|
@@ -175,8 +161,20 @@ something other than the current pin.
|
|
|
175
161
|
|
|
176
162
|
`vendor check` verifies, per addon: the vendored files byte-match the pinned
|
|
177
163
|
commit; a `version` tag (if set) still resolves to that commit; every manifest
|
|
178
|
-
`external_dependencies['python']` is named in `requirements.txt`;
|
|
179
|
-
name collides between `addons/` and `vendored
|
|
164
|
+
`external_dependencies['python']` is named in `requirements.txt`; no addon
|
|
165
|
+
name collides between `addons/` and `vendored/`; and no file under `vendored/` is
|
|
166
|
+
gitignored. Python bytecode (`__pycache__/`, `*.pyc`) is excluded from the byte
|
|
167
|
+
comparison, so running the test suite doesn't turn the check red.
|
|
168
|
+
|
|
169
|
+
That last assertion catches a trap specific to migration: repo-wide ignore rules
|
|
170
|
+
(`node_modules`, `package.json`, …) are harmless while shared addons live in
|
|
171
|
+
submodules, but they start applying the moment those addons become real files
|
|
172
|
+
under `vendored/` — and `git add` then skips the matching files without a word,
|
|
173
|
+
so the committed tree is incomplete while everything looks fine locally.
|
|
174
|
+
`vendor migrate` appends a `!vendored/**` negation to `.gitignore` when it
|
|
175
|
+
detects the situation. Pins themselves come from the gitlink the branch records,
|
|
176
|
+
not from whatever the submodule happens to be checked out at; migrate refuses to
|
|
177
|
+
run against uninitialized submodules.
|
|
180
178
|
|
|
181
179
|
`vendor update` is the **pull** side: each client owns its pins. For every addon
|
|
182
180
|
that tracks upstream — a `version` (bump to the newest `<addon>/<version>` tag) or
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "odoo-dev"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.3"
|
|
4
4
|
description = "Odoo Development Environment Helper"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -12,9 +12,9 @@ dependencies = [
|
|
|
12
12
|
license = "LGPL-3.0"
|
|
13
13
|
|
|
14
14
|
[project.urls]
|
|
15
|
-
Homepage = "https://
|
|
16
|
-
Repository = "https://
|
|
17
|
-
Issues = "https://
|
|
15
|
+
Homepage = "https://github.com/bemade/odoo-dev"
|
|
16
|
+
Repository = "https://github.com/bemade/odoo-dev"
|
|
17
|
+
Issues = "https://github.com/bemade/odoo-dev/issues"
|
|
18
18
|
|
|
19
19
|
[project.scripts]
|
|
20
20
|
odoo-dev = "odoo_dev.cli:app"
|
|
@@ -14,7 +14,7 @@ from typing import Annotated, Optional
|
|
|
14
14
|
import typer
|
|
15
15
|
|
|
16
16
|
from odoo_dev.config import load_config
|
|
17
|
-
from odoo_dev.utils.console import error, info, success
|
|
17
|
+
from odoo_dev.utils.console import error, info, success, warning
|
|
18
18
|
from odoo_dev.vendor.develop import (
|
|
19
19
|
DevelopError,
|
|
20
20
|
develop_state,
|
|
@@ -24,7 +24,12 @@ from odoo_dev.vendor.develop import (
|
|
|
24
24
|
)
|
|
25
25
|
from odoo_dev.vendor.edit import EditError, add_addon, bump_addon
|
|
26
26
|
from odoo_dev.vendor.lock import Lockfile
|
|
27
|
-
from odoo_dev.vendor.migrate import
|
|
27
|
+
from odoo_dev.vendor.migrate import (
|
|
28
|
+
MigrateError,
|
|
29
|
+
ensure_vendored_not_ignored,
|
|
30
|
+
migrate_repo,
|
|
31
|
+
plan_migration,
|
|
32
|
+
)
|
|
28
33
|
from odoo_dev.vendor.sync import sync_addons
|
|
29
34
|
from odoo_dev.vendor.update import apply_update, find_updates
|
|
30
35
|
from odoo_dev.vendor.verify import verify
|
|
@@ -147,8 +152,23 @@ def migrate_cmd(
|
|
|
147
152
|
) -> None:
|
|
148
153
|
"""Convert addons/ symlinks into .repos submodules to vendored/ + addons.lock."""
|
|
149
154
|
cfg = load_config()
|
|
150
|
-
|
|
155
|
+
try:
|
|
151
156
|
plans = plan_migration(cfg.project_dir, addon or None)
|
|
157
|
+
except MigrateError as exc:
|
|
158
|
+
error(str(exc))
|
|
159
|
+
raise typer.Exit(2)
|
|
160
|
+
|
|
161
|
+
# The pin is the gitlink the branch records. Say so when the submodule is
|
|
162
|
+
# checked out somewhere else, since that checkout is what the dev sees.
|
|
163
|
+
for p in plans:
|
|
164
|
+
if p["checkout"] and p["checkout"] != p["commit"]:
|
|
165
|
+
warning(
|
|
166
|
+
f"{p['name']}: {p['sub_path']} is checked out at "
|
|
167
|
+
f"{p['checkout'][:12]} but this branch records "
|
|
168
|
+
f"{p['commit'][:12]} — pinning the recorded commit."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if dry_run:
|
|
152
172
|
if not plans:
|
|
153
173
|
info("Nothing to migrate (no symlinked submodule addons found).")
|
|
154
174
|
return
|
|
@@ -168,6 +188,15 @@ def migrate_cmd(
|
|
|
168
188
|
if cfg.config_file.exists() and cfg.vendored_dir.is_dir():
|
|
169
189
|
if ensure_addons_path(cfg.config_file, str(cfg.vendored_dir)):
|
|
170
190
|
info(f"Added {cfg.vendored_dir} to {cfg.config_file} addons_path.")
|
|
191
|
+
|
|
192
|
+
# Repo-wide ignore rules that were harmless against submodule content now
|
|
193
|
+
# apply to real files under vendored/, where git would drop them from the
|
|
194
|
+
# commit without a word. Negate them at the point of damage.
|
|
195
|
+
if ensure_vendored_not_ignored(cfg.project_dir):
|
|
196
|
+
info(
|
|
197
|
+
"Added a '!vendored/**' negation to .gitignore (rules matched "
|
|
198
|
+
"vendored files, which git would have silently skipped)."
|
|
199
|
+
)
|
|
171
200
|
if unused:
|
|
172
201
|
info("")
|
|
173
202
|
if deinit:
|
|
@@ -5,6 +5,11 @@ modes and symlinks survive verbatim — the vendored copy is byte-identical to t
|
|
|
5
5
|
source subtree. :func:`tree_diff` then compares two trees on content, the
|
|
6
6
|
executable bit, and symlink targets (a plain ``diff -r`` silently ignores the
|
|
7
7
|
latter two and follows symlinks), which is what makes ``vendor check`` trustworthy.
|
|
8
|
+
|
|
9
|
+
Python bytecode (``__pycache__/``, ``*.pyc``, ``*.pyo``) is excluded from the
|
|
10
|
+
comparison: running or testing a vendored addon locally compiles it in place, and
|
|
11
|
+
those artifacts are gitignored build output that is neither part of the pinned
|
|
12
|
+
source nor present in a fresh CI checkout.
|
|
8
13
|
"""
|
|
9
14
|
|
|
10
15
|
from __future__ import annotations
|
|
@@ -60,12 +65,18 @@ def extract_subtree(repo: Path, commit: str, addon: str, dest: Path) -> None:
|
|
|
60
65
|
)
|
|
61
66
|
|
|
62
67
|
|
|
68
|
+
_BYTECODE_DIRS = {"__pycache__"}
|
|
69
|
+
_BYTECODE_SUFFIXES = (".pyc", ".pyo")
|
|
70
|
+
|
|
71
|
+
|
|
63
72
|
def _entries(root: Path) -> dict:
|
|
64
73
|
"""Map each path relative to ``root`` to a comparable fingerprint.
|
|
65
74
|
|
|
66
75
|
- symlink -> ("link", target) (never dereferenced)
|
|
67
76
|
- file -> ("file", exec_bit, sha1)
|
|
68
77
|
- dir -> ("dir",)
|
|
78
|
+
|
|
79
|
+
Python bytecode is skipped entirely (see the module docstring).
|
|
69
80
|
"""
|
|
70
81
|
root = Path(root)
|
|
71
82
|
out: dict = {}
|
|
@@ -73,6 +84,8 @@ def _entries(root: Path) -> dict:
|
|
|
73
84
|
# Do not descend into symlinked directories; record them as links.
|
|
74
85
|
real_dirs = []
|
|
75
86
|
for d in dirnames:
|
|
87
|
+
if d in _BYTECODE_DIRS:
|
|
88
|
+
continue
|
|
76
89
|
p = Path(dirpath) / d
|
|
77
90
|
rel = str(p.relative_to(root))
|
|
78
91
|
if p.is_symlink():
|
|
@@ -82,6 +95,8 @@ def _entries(root: Path) -> dict:
|
|
|
82
95
|
real_dirs.append(d)
|
|
83
96
|
dirnames[:] = real_dirs
|
|
84
97
|
for f in filenames:
|
|
98
|
+
if f.endswith(_BYTECODE_SUFFIXES):
|
|
99
|
+
continue
|
|
85
100
|
p = Path(dirpath) / f
|
|
86
101
|
rel = str(p.relative_to(root))
|
|
87
102
|
if p.is_symlink():
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""``vendor migrate``: convert a submodule+symlink repo to a vendored one.
|
|
2
2
|
|
|
3
3
|
Reads the ``addons/<addon>`` symlinks that point into ``.repos/*`` submodules,
|
|
4
|
-
derives a per-addon pin (submodule remote URL = source,
|
|
4
|
+
derives a per-addon pin (submodule remote URL = source, recorded gitlink = commit,
|
|
5
5
|
manifest version -> version ONLY when the ``<addon>/<version>`` tag resolves to
|
|
6
6
|
the pinned commit; a commit that isn't the tagged release (a pin behind or ahead
|
|
7
7
|
of the tag, or any intermediate commit) is left commit-only, no version),
|
|
@@ -23,12 +23,45 @@ from odoo_dev.utils.manifest import manifest_path, read_version
|
|
|
23
23
|
from odoo_dev.vendor.lock import LockEntry, Lockfile
|
|
24
24
|
from odoo_dev.vendor.sources import tag_resolves_to
|
|
25
25
|
from odoo_dev.vendor.sync import sync_addons
|
|
26
|
+
from odoo_dev.vendor.verify import gitignored_under_vendored
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class MigrateError(Exception):
|
|
29
30
|
pass
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
_NEGATION = "!vendored/**"
|
|
34
|
+
_NEGATION_BLOCK = (
|
|
35
|
+
"\n# Vendored addons must match addons.lock byte-for-byte, so no repo-wide\n"
|
|
36
|
+
"# ignore rule may strip files out of them. Keep last.\n"
|
|
37
|
+
f"{_NEGATION}\n"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ensure_vendored_not_ignored(project_dir: Path) -> bool:
|
|
42
|
+
"""Append a ``!vendored/**`` negation when ``.gitignore`` would eat vendored files.
|
|
43
|
+
|
|
44
|
+
``migrate`` is the command that creates the hazard — it turns submodule content
|
|
45
|
+
(governed by the submodule's own ignore rules) into real files under the
|
|
46
|
+
superproject's — so it is the right place to repair it, in the same shape as
|
|
47
|
+
``ensure_addons_path`` fixing up ``conf/odoo.conf``. Idempotent; returns True
|
|
48
|
+
only when it actually wrote.
|
|
49
|
+
"""
|
|
50
|
+
project_dir = Path(project_dir)
|
|
51
|
+
if not gitignored_under_vendored(project_dir):
|
|
52
|
+
return False
|
|
53
|
+
gitignore = project_dir / ".gitignore"
|
|
54
|
+
text = gitignore.read_text() if gitignore.exists() else ""
|
|
55
|
+
if any(line.strip() == _NEGATION for line in text.splitlines()):
|
|
56
|
+
# Already negated yet still ignored: a later rule re-excludes the path, or
|
|
57
|
+
# the negation cannot apply. Leave it — `vendor check` reports the files.
|
|
58
|
+
return False
|
|
59
|
+
if text and not text.endswith("\n"):
|
|
60
|
+
text += "\n"
|
|
61
|
+
gitignore.write_text(text + _NEGATION_BLOCK)
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
|
|
32
65
|
def read_gitmodules(project_dir: Path) -> list:
|
|
33
66
|
"""Return [(name, path, url)] for each submodule declared in .gitmodules."""
|
|
34
67
|
gm = Path(project_dir) / ".gitmodules"
|
|
@@ -57,19 +90,82 @@ def read_gitmodules(project_dir: Path) -> list:
|
|
|
57
90
|
]
|
|
58
91
|
|
|
59
92
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
93
|
+
def _gitlink_commit(project_dir: Path, sub_path: str) -> str:
|
|
94
|
+
"""The commit the SUPERPROJECT records for ``sub_path`` — the authoritative pin.
|
|
95
|
+
|
|
96
|
+
Read from the superproject tree, never from the submodule working copy. An
|
|
97
|
+
uninitialized submodule is an empty directory, so ``git -C <sub> rev-parse
|
|
98
|
+
HEAD`` walks up to the enclosing repo, returns the SUPERPROJECT's head and
|
|
99
|
+
exits 0 — pinning every addon across every source repo to one wrong sha, in a
|
|
100
|
+
lockfile that ``vendor check`` then happily validates against itself.
|
|
101
|
+
``ls-tree`` cannot walk up, and the ``160000`` mode proves it is a gitlink.
|
|
102
|
+
"""
|
|
103
|
+
res = subprocess.run(
|
|
104
|
+
["git", "-C", str(project_dir), "ls-tree", "HEAD", "--", sub_path],
|
|
64
105
|
capture_output=True,
|
|
65
106
|
text=True,
|
|
66
|
-
)
|
|
107
|
+
)
|
|
108
|
+
line = res.stdout.strip()
|
|
109
|
+
if res.returncode != 0 or not line:
|
|
110
|
+
raise MigrateError(
|
|
111
|
+
f"{sub_path}: no submodule recorded at this path in HEAD — cannot "
|
|
112
|
+
f"derive a pin (commit the submodule, or fix .gitmodules)."
|
|
113
|
+
)
|
|
114
|
+
mode, _, rest = line.partition(" ")
|
|
115
|
+
obj_type, _, rest = rest.partition(" ")
|
|
116
|
+
sha = rest.split("\t", 1)[0].strip()
|
|
117
|
+
if mode != "160000" or obj_type != "commit":
|
|
118
|
+
raise MigrateError(
|
|
119
|
+
f"{sub_path}: recorded in HEAD as a {obj_type}, not a submodule "
|
|
120
|
+
f"gitlink — cannot derive a pin."
|
|
121
|
+
)
|
|
122
|
+
return sha
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _submodule_checkout(project_dir: Path, sub_path: str) -> Optional[str]:
|
|
126
|
+
"""The commit the submodule working copy is actually on, or None if absent.
|
|
127
|
+
|
|
128
|
+
Only meaningful for an initialized submodule (which owns a ``.git``); the
|
|
129
|
+
caller uses it to warn when the checkout has drifted off the gitlink.
|
|
130
|
+
"""
|
|
131
|
+
sub_root = Path(project_dir) / sub_path
|
|
132
|
+
if not (sub_root / ".git").exists():
|
|
133
|
+
return None
|
|
134
|
+
res = subprocess.run(
|
|
135
|
+
["git", "-C", str(sub_root), "rev-parse", "HEAD"],
|
|
136
|
+
capture_output=True,
|
|
137
|
+
text=True,
|
|
138
|
+
)
|
|
139
|
+
return res.stdout.strip() if res.returncode == 0 else None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def uninitialized_submodules(project_dir: Path) -> list:
|
|
143
|
+
"""Submodule paths git reports as not checked out (``-`` prefix)."""
|
|
144
|
+
res = subprocess.run(
|
|
145
|
+
["git", "-C", str(project_dir), "submodule", "status"],
|
|
146
|
+
capture_output=True,
|
|
147
|
+
text=True,
|
|
148
|
+
)
|
|
149
|
+
paths = []
|
|
150
|
+
for line in res.stdout.splitlines():
|
|
151
|
+
if not line.startswith("-"):
|
|
152
|
+
continue
|
|
153
|
+
parts = line[1:].split()
|
|
154
|
+
if len(parts) >= 2:
|
|
155
|
+
paths.append(parts[1])
|
|
156
|
+
return paths
|
|
67
157
|
|
|
68
158
|
|
|
69
159
|
def plan_migration(project_dir: Path, addons: Optional[Iterable[str]] = None) -> list:
|
|
70
160
|
"""Return a list of planned pins without changing anything.
|
|
71
161
|
|
|
72
|
-
Each item: dict(name, source, commit, version, symlink, sub_path).
|
|
162
|
+
Each item: dict(name, source, commit, version, checkout, symlink, sub_path).
|
|
163
|
+
``checkout`` is the submodule's actual HEAD, so the caller can warn when it
|
|
164
|
+
has drifted off the recorded gitlink the pin comes from.
|
|
165
|
+
|
|
166
|
+
Raises :class:`MigrateError` if any submodule being migrated is uninitialized:
|
|
167
|
+
its addon content and manifest are simply not on disk, and its pin cannot be
|
|
168
|
+
trusted to mean anything.
|
|
73
169
|
"""
|
|
74
170
|
project_dir = Path(project_dir)
|
|
75
171
|
addons_dir = project_dir / "addons"
|
|
@@ -77,6 +173,8 @@ def plan_migration(project_dir: Path, addons: Optional[Iterable[str]] = None) ->
|
|
|
77
173
|
return []
|
|
78
174
|
subs = read_gitmodules(project_dir)
|
|
79
175
|
only = set(addons) if addons is not None else None
|
|
176
|
+
uninit = set(uninitialized_submodules(project_dir))
|
|
177
|
+
blocked = []
|
|
80
178
|
|
|
81
179
|
plans = []
|
|
82
180
|
for link in sorted(addons_dir.iterdir()):
|
|
@@ -99,6 +197,9 @@ def plan_migration(project_dir: Path, addons: Optional[Iterable[str]] = None) ->
|
|
|
99
197
|
if sub is None:
|
|
100
198
|
continue # symlink not into a known submodule; skip
|
|
101
199
|
s_name, s_path, s_url = sub
|
|
200
|
+
if s_path in uninit:
|
|
201
|
+
blocked.append((name, s_path))
|
|
202
|
+
continue
|
|
102
203
|
sub_root = (project_dir / s_path).resolve()
|
|
103
204
|
subpath = real.relative_to(sub_root)
|
|
104
205
|
if str(subpath) != name:
|
|
@@ -106,7 +207,7 @@ def plan_migration(project_dir: Path, addons: Optional[Iterable[str]] = None) ->
|
|
|
106
207
|
f"{name}: addon sits at '{subpath}' inside submodule {s_path}, not at "
|
|
107
208
|
f"the root as '{name}'. Nested addons aren't handled yet — migrate by hand."
|
|
108
209
|
)
|
|
109
|
-
commit =
|
|
210
|
+
commit = _gitlink_commit(project_dir, s_path)
|
|
110
211
|
version = None
|
|
111
212
|
mf = manifest_path(real)
|
|
112
213
|
if mf is not None:
|
|
@@ -125,10 +226,20 @@ def plan_migration(project_dir: Path, addons: Optional[Iterable[str]] = None) ->
|
|
|
125
226
|
source=s_url,
|
|
126
227
|
commit=commit,
|
|
127
228
|
version=version,
|
|
229
|
+
checkout=_submodule_checkout(project_dir, s_path),
|
|
128
230
|
symlink=link,
|
|
129
231
|
sub_path=s_path,
|
|
130
232
|
)
|
|
131
233
|
)
|
|
234
|
+
|
|
235
|
+
if blocked:
|
|
236
|
+
paths = sorted({s_path for _, s_path in blocked})
|
|
237
|
+
names = ", ".join(sorted(name for name, _ in blocked))
|
|
238
|
+
raise MigrateError(
|
|
239
|
+
f"uninitialized submodule(s): {', '.join(paths)} — needed by {names}. "
|
|
240
|
+
f"Run 'git submodule update --init' first; migrating without them "
|
|
241
|
+
f"would pin every addon to the superproject's own HEAD."
|
|
242
|
+
)
|
|
132
243
|
return plans
|
|
133
244
|
|
|
134
245
|
|
|
@@ -6,7 +6,8 @@ Assertions (any failure => non-empty problem list => CI red):
|
|
|
6
6
|
3. moved-tag tripwire (when the entry carries a ``version`` tag);
|
|
7
7
|
4. external python deps of vendored manifests are present in ``requirements.txt``;
|
|
8
8
|
5. no addon name in both ``addons/`` and ``vendored/`` (double-load);
|
|
9
|
-
6.
|
|
9
|
+
6. no file under ``vendored/`` is gitignored (it would be dropped at commit);
|
|
10
|
+
7. (``allow_hybrid=False`` only) no ``addons/`` symlink still points into a
|
|
10
11
|
``.repos/`` submodule — i.e. the repo is fully vendored, not a hybrid.
|
|
11
12
|
The gate writes nothing.
|
|
12
13
|
"""
|
|
@@ -16,6 +17,7 @@ from __future__ import annotations
|
|
|
16
17
|
import ast
|
|
17
18
|
import os
|
|
18
19
|
import re
|
|
20
|
+
import subprocess
|
|
19
21
|
import tempfile
|
|
20
22
|
from pathlib import Path
|
|
21
23
|
from typing import Optional
|
|
@@ -108,6 +110,45 @@ def hybrid_submodule_addons(project_dir: Path) -> list:
|
|
|
108
110
|
return hybrids
|
|
109
111
|
|
|
110
112
|
|
|
113
|
+
def gitignored_under_vendored(project_dir: Path) -> list:
|
|
114
|
+
"""Paths under ``vendored/`` that git would silently refuse to add.
|
|
115
|
+
|
|
116
|
+
Assertions 1-2 compare the pinned subtree against the WORKING TREE, which
|
|
117
|
+
``sync``/``migrate`` just materialized in full — so they always match, and are
|
|
118
|
+
blind to a repo-wide ignore rule (``node_modules``, ``package.json``, …) that
|
|
119
|
+
is harmless while shared addons live in submodules but starts applying the
|
|
120
|
+
moment they become real files here. ``git add`` then skips those files without
|
|
121
|
+
a word and the committed tree is incomplete: green locally, red only once CI
|
|
122
|
+
checks out a fresh copy, with an error that reads like an upstream problem.
|
|
123
|
+
|
|
124
|
+
``check-ignore`` (not a tracked-vs-on-disk comparison) is the right probe: it
|
|
125
|
+
reports exactly the paths git would drop, and consults the index, so neither a
|
|
126
|
+
not-yet-staged ``vendor add`` nor a deliberate ``git add -f`` is a false alarm.
|
|
127
|
+
"""
|
|
128
|
+
project_dir = Path(project_dir)
|
|
129
|
+
vendored = project_dir / "vendored"
|
|
130
|
+
if not vendored.is_dir() or not (project_dir / ".git").exists():
|
|
131
|
+
return []
|
|
132
|
+
rels = [
|
|
133
|
+
str(p.relative_to(project_dir))
|
|
134
|
+
for p in sorted(vendored.rglob("*"))
|
|
135
|
+
if p.is_symlink() or p.is_file()
|
|
136
|
+
]
|
|
137
|
+
if not rels:
|
|
138
|
+
return []
|
|
139
|
+
res = subprocess.run(
|
|
140
|
+
["git", "-C", str(project_dir), "check-ignore", "--stdin"],
|
|
141
|
+
input="\n".join(rels) + "\n",
|
|
142
|
+
capture_output=True,
|
|
143
|
+
text=True,
|
|
144
|
+
)
|
|
145
|
+
# 0 = at least one path is ignored, 1 = none are; anything else is an error
|
|
146
|
+
# (no git, not a work tree) and must not fail the gate on its own.
|
|
147
|
+
if res.returncode not in (0, 1):
|
|
148
|
+
return []
|
|
149
|
+
return [line for line in res.stdout.splitlines() if line.strip()]
|
|
150
|
+
|
|
151
|
+
|
|
111
152
|
def verify(
|
|
112
153
|
project_dir: Path,
|
|
113
154
|
lock: Lockfile,
|
|
@@ -179,6 +220,13 @@ def verify(
|
|
|
179
220
|
f"{name}: present in BOTH addons/ and vendored/ (double-load)"
|
|
180
221
|
)
|
|
181
222
|
|
|
223
|
+
for rel in gitignored_under_vendored(project_dir):
|
|
224
|
+
problems.append(
|
|
225
|
+
f"{rel}: under vendored/ but ignored by .gitignore — git will drop it "
|
|
226
|
+
f"from the commit and the pushed tree will not match addons.lock "
|
|
227
|
+
f"(add a '!vendored/**' negation, keep it last)"
|
|
228
|
+
)
|
|
229
|
+
|
|
182
230
|
if not allow_hybrid:
|
|
183
231
|
for name in hybrid_submodule_addons(project_dir):
|
|
184
232
|
problems.append(
|
|
@@ -137,6 +137,37 @@ def test_tree_diff_detects_missing_and_extra_files(tmp_path):
|
|
|
137
137
|
assert any("thing.py" in d for d in diffs)
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
def test_tree_diff_ignores_bytecode_artifacts(tmp_path):
|
|
141
|
+
"""Running the addon compiles it; ``__pycache__`` is not part of the pin.
|
|
142
|
+
|
|
143
|
+
The .pyc are gitignored and absent from a fresh CI checkout, so treating them
|
|
144
|
+
as "extra" made ``vendor check`` fail locally right after ``odoo-dev test``.
|
|
145
|
+
"""
|
|
146
|
+
repo, sha = _make_source_repo(tmp_path)
|
|
147
|
+
a = tmp_path / "a"
|
|
148
|
+
b = tmp_path / "b"
|
|
149
|
+
extract_subtree(repo, sha, "my_addon", a)
|
|
150
|
+
extract_subtree(repo, sha, "my_addon", b)
|
|
151
|
+
cache = b / "models" / "__pycache__"
|
|
152
|
+
cache.mkdir()
|
|
153
|
+
(cache / "thing.cpython-312.pyc").write_bytes(b"\x00compiled")
|
|
154
|
+
(b / "models" / "thing.pyo").write_bytes(b"\x00compiled")
|
|
155
|
+
assert tree_diff(a, b) == []
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_tree_diff_still_detects_real_change_beside_bytecode(tmp_path):
|
|
159
|
+
"""Ignoring bytecode must not mask a genuine drift in the same directory."""
|
|
160
|
+
repo, sha = _make_source_repo(tmp_path)
|
|
161
|
+
a = tmp_path / "a"
|
|
162
|
+
b = tmp_path / "b"
|
|
163
|
+
extract_subtree(repo, sha, "my_addon", a)
|
|
164
|
+
extract_subtree(repo, sha, "my_addon", b)
|
|
165
|
+
(b / "models" / "__pycache__").mkdir()
|
|
166
|
+
(b / "models" / "__pycache__" / "thing.cpython-312.pyc").write_bytes(b"\x00")
|
|
167
|
+
(b / "models" / "thing.py").write_text("x = 2\n")
|
|
168
|
+
assert any("thing.py" in d for d in tree_diff(a, b))
|
|
169
|
+
|
|
170
|
+
|
|
140
171
|
def test_extract_bad_commit_raises(tmp_path):
|
|
141
172
|
repo, _ = _make_source_repo(tmp_path)
|
|
142
173
|
with pytest.raises(Exception):
|
|
@@ -6,9 +6,14 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
from odoo_dev.vendor.migrate import
|
|
9
|
+
from odoo_dev.vendor.migrate import (
|
|
10
|
+
MigrateError,
|
|
11
|
+
ensure_vendored_not_ignored,
|
|
12
|
+
migrate_repo,
|
|
13
|
+
plan_migration,
|
|
14
|
+
)
|
|
10
15
|
from odoo_dev.vendor.lock import Lockfile
|
|
11
|
-
from odoo_dev.vendor.verify import verify
|
|
16
|
+
from odoo_dev.vendor.verify import gitignored_under_vendored, verify
|
|
12
17
|
|
|
13
18
|
|
|
14
19
|
def _git(repo: Path, *args: str) -> str:
|
|
@@ -171,3 +176,81 @@ def test_migrate_is_idempotent(tmp_path):
|
|
|
171
176
|
# second run: symlink already gone -> nothing to plan, no error
|
|
172
177
|
lock, unused = migrate_repo(client, cache_dir=tmp_path / "cache")
|
|
173
178
|
assert unused == []
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# --- pins come from the recorded gitlink, never the submodule working copy -----
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_plan_hard_fails_on_uninitialized_submodule(tmp_path):
|
|
185
|
+
"""Regression: an uninitialized submodule is an EMPTY DIR, so
|
|
186
|
+
``git -C <sub> rev-parse HEAD`` walks up to the superproject, returns ITS head
|
|
187
|
+
and exits 0. Every addon across every source repo got pinned to that one sha,
|
|
188
|
+
and the lock looked perfectly valid.
|
|
189
|
+
"""
|
|
190
|
+
source, sha = _source_repo(tmp_path)
|
|
191
|
+
client = _client_repo(tmp_path, source)
|
|
192
|
+
_git(client, "submodule", "deinit", "-f", ".repos/sale-workflow")
|
|
193
|
+
(client / ".repos" / "sale-workflow").mkdir(exist_ok=True)
|
|
194
|
+
superproject_head = _git(client, "rev-parse", "HEAD")
|
|
195
|
+
|
|
196
|
+
with pytest.raises(MigrateError) as exc:
|
|
197
|
+
plan_migration(client)
|
|
198
|
+
assert ".repos/sale-workflow" in str(exc.value)
|
|
199
|
+
assert "submodule update --init" in str(exc.value)
|
|
200
|
+
# and the sha that WOULD have been written is not the superproject's
|
|
201
|
+
assert sha != superproject_head
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_plan_pins_recorded_gitlink_not_local_checkout(tmp_path):
|
|
205
|
+
"""A submodule checked out somewhere other than the recorded gitlink (a common
|
|
206
|
+
state while debugging a shared addon) must not silently redefine the pin: the
|
|
207
|
+
lock has to describe the branch being migrated.
|
|
208
|
+
"""
|
|
209
|
+
source, first = _source_repo(tmp_path)
|
|
210
|
+
client = _client_repo(tmp_path, source) # gitlink records `first`
|
|
211
|
+
# move the source forward and check the submodule out there, without
|
|
212
|
+
# committing the gitlink bump in the superproject
|
|
213
|
+
(source / "shared_addon" / "models" / "m.py").write_text("z = 99\n")
|
|
214
|
+
_git(source, "add", "-A")
|
|
215
|
+
_git(source, "commit", "-qm", "local debugging")
|
|
216
|
+
later = _git(source, "rev-parse", "HEAD")
|
|
217
|
+
sub = client / ".repos" / "sale-workflow"
|
|
218
|
+
_git(sub, "fetch", "-q", "origin")
|
|
219
|
+
_git(sub, "checkout", "-q", later)
|
|
220
|
+
|
|
221
|
+
plans = plan_migration(client)
|
|
222
|
+
assert plans[0]["commit"] == first # the gitlink, not the checkout
|
|
223
|
+
assert plans[0]["checkout"] == later # surfaced so the CLI can warn
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# --- migrate repairs the .gitignore it just made dangerous --------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _vendored_project(tmp_path: Path, gitignore: str) -> Path:
|
|
230
|
+
proj = tmp_path / "proj"
|
|
231
|
+
(proj / "vendored" / "shared_addon" / "static").mkdir(parents=True)
|
|
232
|
+
(proj / "vendored" / "shared_addon" / "static" / "package.json").write_text("{}\n")
|
|
233
|
+
_git(proj, "init", "-q")
|
|
234
|
+
(proj / ".gitignore").write_text(gitignore)
|
|
235
|
+
return proj
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_ensure_vendored_not_ignored_appends_negation(tmp_path):
|
|
239
|
+
proj = _vendored_project(tmp_path, "node_modules\npackage.json\n")
|
|
240
|
+
assert gitignored_under_vendored(proj) # the trap is armed
|
|
241
|
+
|
|
242
|
+
assert ensure_vendored_not_ignored(proj) is True
|
|
243
|
+
assert "!vendored/**" in (proj / ".gitignore").read_text()
|
|
244
|
+
assert gitignored_under_vendored(proj) == [] # and disarmed
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_ensure_vendored_not_ignored_is_idempotent(tmp_path):
|
|
248
|
+
proj = _vendored_project(tmp_path, "node_modules\npackage.json\n")
|
|
249
|
+
assert ensure_vendored_not_ignored(proj) is True
|
|
250
|
+
assert ensure_vendored_not_ignored(proj) is False
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_ensure_vendored_not_ignored_leaves_a_clean_repo_alone(tmp_path):
|
|
254
|
+
proj = _vendored_project(tmp_path, "*.pyc\n")
|
|
255
|
+
assert ensure_vendored_not_ignored(proj) is False
|
|
256
|
+
assert "!vendored/**" not in (proj / ".gitignore").read_text()
|
|
@@ -17,9 +17,17 @@ def _git(repo: Path, *args: str) -> str:
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def _source_repo(
|
|
20
|
-
tmp_path: Path,
|
|
20
|
+
tmp_path: Path,
|
|
21
|
+
*,
|
|
22
|
+
python_dep: str | None = None,
|
|
23
|
+
tag: str | None = None,
|
|
24
|
+
extra: dict | None = None,
|
|
21
25
|
):
|
|
22
|
-
"""A source repo with addon ``shared_addon``; optional python dep + version tag.
|
|
26
|
+
"""A source repo with addon ``shared_addon``; optional python dep + version tag.
|
|
27
|
+
|
|
28
|
+
``extra`` maps addon-relative paths to contents, for files whose NAME is what
|
|
29
|
+
matters (e.g. a bundled ``package.json`` a repo-wide ignore rule would eat).
|
|
30
|
+
"""
|
|
23
31
|
repo = tmp_path / "src"
|
|
24
32
|
(repo / "shared_addon").mkdir(parents=True)
|
|
25
33
|
manifest = {"name": "shared_addon", "version": "18.0.1.0.0"}
|
|
@@ -27,6 +35,10 @@ def _source_repo(
|
|
|
27
35
|
manifest["external_dependencies"] = {"python": [python_dep]}
|
|
28
36
|
(repo / "shared_addon" / "__manifest__.py").write_text(repr(manifest) + "\n")
|
|
29
37
|
(repo / "shared_addon" / "models.py").write_text("y = 2\n")
|
|
38
|
+
for rel, content in (extra or {}).items():
|
|
39
|
+
p = repo / "shared_addon" / rel
|
|
40
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
p.write_text(content)
|
|
30
42
|
_git(repo, "init", "-q")
|
|
31
43
|
_git(repo, "config", "user.email", "t@t")
|
|
32
44
|
_git(repo, "config", "user.name", "t")
|
|
@@ -171,6 +183,59 @@ def test_verify_strict_green_on_fully_vendored_repo(tmp_path):
|
|
|
171
183
|
assert verify(proj, lock, cache_dir=tmp_path / "cache", allow_hybrid=False) == []
|
|
172
184
|
|
|
173
185
|
|
|
186
|
+
# --- .gitignore silently stripping files out of vendored/ ---------------------
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _git_project(tmp_path: Path, entry: LockEntry, gitignore: str) -> tuple:
|
|
190
|
+
proj, lock = _project(tmp_path, entry)
|
|
191
|
+
_git(proj, "init", "-q")
|
|
192
|
+
_git(proj, "config", "user.email", "t@t")
|
|
193
|
+
_git(proj, "config", "user.name", "t")
|
|
194
|
+
(proj / ".gitignore").write_text(gitignore)
|
|
195
|
+
return proj, lock
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_verify_red_when_gitignore_would_drop_vendored_files(tmp_path):
|
|
199
|
+
"""Regression: repo-wide js-tooling ignores are harmless while shared addons
|
|
200
|
+
live in submodules, but start applying the moment they become real files under
|
|
201
|
+
vendored/. ``git add`` then skips them silently and the committed tree is
|
|
202
|
+
incomplete — green locally, red only in CI, with an error that reads like an
|
|
203
|
+
upstream problem.
|
|
204
|
+
"""
|
|
205
|
+
repo, sha = _source_repo(
|
|
206
|
+
tmp_path, extra={"static/src/lib/vendorlib/package.json": "{}\n"}
|
|
207
|
+
)
|
|
208
|
+
proj, lock = _git_project(
|
|
209
|
+
tmp_path,
|
|
210
|
+
LockEntry("shared_addon", str(repo), sha),
|
|
211
|
+
"node_modules\npackage.json\n",
|
|
212
|
+
)
|
|
213
|
+
sync_addons(proj, lock, cache_dir=tmp_path / "cache")
|
|
214
|
+
problems = verify(proj, lock, cache_dir=tmp_path / "cache")
|
|
215
|
+
assert any("package.json" in p and ".gitignore" in p for p in problems)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_verify_green_when_vendored_negation_present(tmp_path):
|
|
219
|
+
repo, sha = _source_repo(
|
|
220
|
+
tmp_path, extra={"static/src/lib/vendorlib/package.json": "{}\n"}
|
|
221
|
+
)
|
|
222
|
+
proj, lock = _git_project(
|
|
223
|
+
tmp_path,
|
|
224
|
+
LockEntry("shared_addon", str(repo), sha),
|
|
225
|
+
"node_modules\npackage.json\n!vendored/**\n",
|
|
226
|
+
)
|
|
227
|
+
sync_addons(proj, lock, cache_dir=tmp_path / "cache")
|
|
228
|
+
assert verify(proj, lock, cache_dir=tmp_path / "cache") == []
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_verify_gitignore_check_is_silent_outside_a_git_repo(tmp_path):
|
|
232
|
+
"""A plain (non-git) project dir must not trip the new assertion."""
|
|
233
|
+
repo, sha = _source_repo(tmp_path, extra={"package.json": "{}\n"})
|
|
234
|
+
proj, lock = _project(tmp_path, LockEntry("shared_addon", str(repo), sha))
|
|
235
|
+
sync_addons(proj, lock, cache_dir=tmp_path / "cache")
|
|
236
|
+
assert verify(proj, lock, cache_dir=tmp_path / "cache") == []
|
|
237
|
+
|
|
238
|
+
|
|
174
239
|
def test_verify_strict_ignores_non_repos_symlink(tmp_path):
|
|
175
240
|
proj, lock = _sync_clean(tmp_path)
|
|
176
241
|
# A symlink that does NOT point into .repos/ is not a hybrid.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|