odoo-dev 1.5.0__tar.gz → 1.6.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.
Files changed (71) hide show
  1. odoo_dev-1.6.0/.release-please-manifest.json +3 -0
  2. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/CHANGELOG.md +14 -0
  3. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/PKG-INFO +11 -1
  4. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/README.md +10 -0
  5. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/pyproject.toml +1 -1
  6. odoo_dev-1.6.0/src/odoo_dev/__init__.py +3 -0
  7. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/commands/vendor.py +23 -2
  8. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/vendor/edit.py +9 -1
  9. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/vendor/lock.py +6 -0
  10. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/vendor/sources.py +46 -4
  11. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/vendor/verify.py +21 -1
  12. odoo_dev-1.6.0/tests/test_vendor_mainline.py +107 -0
  13. odoo_dev-1.6.0/tests/test_vendor_sources.py +58 -0
  14. odoo_dev-1.5.0/.release-please-manifest.json +0 -3
  15. odoo_dev-1.5.0/src/odoo_dev/__init__.py +0 -3
  16. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/.github/workflows/release-please.yml +0 -0
  17. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/.gitignore +0 -0
  18. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/.gitmodules +0 -0
  19. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/.python-version +0 -0
  20. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/release-please-config.json +0 -0
  21. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/cli.py +0 -0
  22. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/commands/__init__.py +0 -0
  23. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/commands/bump.py +0 -0
  24. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/commands/db.py +0 -0
  25. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/commands/docker.py +0 -0
  26. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/commands/git.py +0 -0
  27. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/commands/run.py +0 -0
  28. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/commands/setup.py +0 -0
  29. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/config.py +0 -0
  30. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/preflight.py +0 -0
  31. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/templates/__init__.py +0 -0
  32. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/templates/docker/Dockerfile +0 -0
  33. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/templates/docker/__init__.py +0 -0
  34. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/templates/docker/docker-entrypoint.sh +0 -0
  35. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/templates/vscode/__init__.py +0 -0
  36. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/templates/vscode/launch.json +0 -0
  37. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/templates/vscode/settings.json +0 -0
  38. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/templates/vscode/tasks.json +0 -0
  39. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/utils/__init__.py +0 -0
  40. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/utils/console.py +0 -0
  41. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/utils/manifest.py +0 -0
  42. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/vendor/__init__.py +0 -0
  43. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/vendor/develop.py +0 -0
  44. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/vendor/materialize.py +0 -0
  45. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/vendor/migrate.py +0 -0
  46. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/vendor/sync.py +0 -0
  47. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/src/odoo_dev/vendor/update.py +0 -0
  48. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/__init__.py +0 -0
  49. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/fixtures/__init__.py +0 -0
  50. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_bump.py +0 -0
  51. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_cli.py +0 -0
  52. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_config.py +0 -0
  53. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_db.py +0 -0
  54. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_db_config.py +0 -0
  55. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_drop_database.py +0 -0
  56. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_drop_filestore.py +0 -0
  57. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_git_hygiene.py +0 -0
  58. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_integration.py +0 -0
  59. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_preflight.py +0 -0
  60. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_run.py +0 -0
  61. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_setup.py +0 -0
  62. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_setup_data_dir.py +0 -0
  63. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_setup_db.py +0 -0
  64. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_vendor_develop.py +0 -0
  65. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_vendor_edit.py +0 -0
  66. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_vendor_lock.py +0 -0
  67. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_vendor_materialize.py +0 -0
  68. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_vendor_migrate.py +0 -0
  69. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_vendor_update.py +0 -0
  70. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/tests/test_vendor_verify.py +0 -0
  71. {odoo_dev-1.5.0 → odoo_dev-1.6.0}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.6.0"
3
+ }
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.0](https://github.com/bemade/odoo-dev/compare/v1.5.1...v1.6.0) (2026-10-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * **vendor:** pin upstream feature branches, gate production on merged pins ([#28](https://github.com/bemade/odoo-dev/issues/28)) ([e232daa](https://github.com/bemade/odoo-dev/commit/e232daa1eb0b040445f97046bdc79ef745153298))
9
+
10
+ ## [1.5.1](https://github.com/bemade/odoo-dev/compare/v1.5.0...v1.5.1) (2026-10-02)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **vendor:** resolve --branch to the fetched remote tip, not the clone-time local branch ([#26](https://github.com/bemade/odoo-dev/issues/26)) ([af03080](https://github.com/bemade/odoo-dev/commit/af030803ba92ba140c875d728c22994eb9271d39))
16
+
3
17
  ## [1.5.0](https://github.com/bemade/odoo-dev/compare/v1.4.0...v1.5.0) (2026-10-01)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: odoo-dev
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Odoo Development Environment Helper
5
5
  Project-URL: Homepage, https://github.com/bemade/odoo-dev
6
6
  Project-URL: Repository, https://github.com/bemade/odoo-dev
@@ -156,6 +156,8 @@ odoo-dev vendor sync # Materialize vendored/ from addons.lock
156
156
  odoo-dev vendor check # CI gate: verify vendored/ byte-matches the pins
157
157
  odoo-dev vendor add fsm --source github.com/bemade/bemade-addons --version 18.0.1.3.2
158
158
  odoo-dev vendor bump fsm --version 18.0.1.4.0 # Move a pin and re-materialize
159
+ odoo-dev vendor bump fsm --branch 18.0-fix-x # Track a branch, pin its head
160
+ odoo-dev vendor check --mainline 18.0 # Production gate: pins must be merged upstream
159
161
  odoo-dev vendor update # Pull newest upstream for all tracked addons
160
162
  odoo-dev vendor update fsm --dry-run # Show what would update, change nothing
161
163
  odoo-dev vendor develop fsm # Edit fsm against a live source clone
@@ -181,6 +183,14 @@ name collides between `addons/` and `vendored/`; and no file under `vendored/` i
181
183
  gitignored. Python bytecode (`__pycache__/`, `*.pyc`) is excluded from the byte
182
184
  comparison, so running the test suite doesn't turn the check red.
183
185
 
186
+ `vendor check --mainline <branch>` (for merge requests to production) also fails
187
+ any pin whose commit is not in the history of that branch of its source — an
188
+ upstream change still under test on a feature branch. A client can pin a feature
189
+ branch while the change is tested on its staging (`vendor bump <addon> --branch
190
+ <feature>`), but production only takes it once it is merged upstream and
191
+ re-pinned. An entry that is meant to live on a branch (a long-lived fork) opts
192
+ out with `allow_unmerged: true` in `addons.lock`.
193
+
184
194
  That last assertion catches a trap specific to migration: repo-wide ignore rules
185
195
  (`node_modules`, `package.json`, …) are harmless while shared addons live in
186
196
  submodules, but they start applying the moment those addons become real files
@@ -141,6 +141,8 @@ odoo-dev vendor sync # Materialize vendored/ from addons.lock
141
141
  odoo-dev vendor check # CI gate: verify vendored/ byte-matches the pins
142
142
  odoo-dev vendor add fsm --source github.com/bemade/bemade-addons --version 18.0.1.3.2
143
143
  odoo-dev vendor bump fsm --version 18.0.1.4.0 # Move a pin and re-materialize
144
+ odoo-dev vendor bump fsm --branch 18.0-fix-x # Track a branch, pin its head
145
+ odoo-dev vendor check --mainline 18.0 # Production gate: pins must be merged upstream
144
146
  odoo-dev vendor update # Pull newest upstream for all tracked addons
145
147
  odoo-dev vendor update fsm --dry-run # Show what would update, change nothing
146
148
  odoo-dev vendor develop fsm # Edit fsm against a live source clone
@@ -166,6 +168,14 @@ name collides between `addons/` and `vendored/`; and no file under `vendored/` i
166
168
  gitignored. Python bytecode (`__pycache__/`, `*.pyc`) is excluded from the byte
167
169
  comparison, so running the test suite doesn't turn the check red.
168
170
 
171
+ `vendor check --mainline <branch>` (for merge requests to production) also fails
172
+ any pin whose commit is not in the history of that branch of its source — an
173
+ upstream change still under test on a feature branch. A client can pin a feature
174
+ branch while the change is tested on its staging (`vendor bump <addon> --branch
175
+ <feature>`), but production only takes it once it is merged upstream and
176
+ re-pinned. An entry that is meant to live on a branch (a long-lived fork) opts
177
+ out with `allow_unmerged: true` in `addons.lock`.
178
+
169
179
  That last assertion catches a trap specific to migration: repo-wide ignore rules
170
180
  (`node_modules`, `package.json`, …) are harmless while shared addons live in
171
181
  submodules, but they start applying the moment those addons become real files
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "odoo-dev"
3
- version = "1.5.0"
3
+ version = "1.6.0"
4
4
  description = "Odoo Development Environment Helper"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,3 @@
1
+ """Odoo Development Environment Helper."""
2
+
3
+ __version__ = "1.6.0" # x-release-please-version
@@ -70,11 +70,23 @@ def check_cmd(
70
70
  ".repos/). Use in CI for repos that declare themselves fully vendored.",
71
71
  ),
72
72
  ] = False,
73
+ mainline: Annotated[
74
+ Optional[str],
75
+ typer.Option(
76
+ "--mainline",
77
+ help="Also fail any pin whose commit is not on this branch of its "
78
+ "source (e.g. 19.0): an upstream change still on a feature branch. "
79
+ "Use in CI on merge requests to production. Entries marked "
80
+ "'allow_unmerged: true' are exempt.",
81
+ ),
82
+ ] = None,
73
83
  ) -> None:
74
84
  """Verify vendored/ matches addons.lock exactly (the CI gate). Exit 1 on any problem."""
75
85
  cfg = load_config()
76
86
  lock = Lockfile.load(cfg.lockfile_path)
77
- problems = verify(cfg.project_dir, lock, allow_hybrid=not no_hybrid)
87
+ problems = verify(
88
+ cfg.project_dir, lock, allow_hybrid=not no_hybrid, mainline=mainline
89
+ )
78
90
  if problems:
79
91
  for p in problems:
80
92
  error(p)
@@ -124,11 +136,20 @@ def bump_cmd(
124
136
  commit: Annotated[
125
137
  Optional[str], typer.Option("--commit", help="Bump to an explicit commit/ref.")
126
138
  ] = None,
139
+ branch: Annotated[
140
+ Optional[str],
141
+ typer.Option(
142
+ "--branch",
143
+ help="Track this branch from now on (e.g. an upstream feature branch "
144
+ "under test, or the mainline once merged); without --version/--commit "
145
+ "the pin moves to its head.",
146
+ ),
147
+ ] = None,
127
148
  ) -> None:
128
149
  """Move an addon's pin to a new version/commit (or its tracked branch HEAD) and re-sync."""
129
150
  cfg = load_config()
130
151
  try:
131
- entry = bump_addon(cfg.project_dir, name, version, commit)
152
+ entry = bump_addon(cfg.project_dir, name, version, commit, branch=branch)
132
153
  except EditError as exc:
133
154
  error(str(exc))
134
155
  raise typer.Exit(2)
@@ -67,13 +67,21 @@ def bump_addon(
67
67
  version: Optional[str] = None,
68
68
  commit: Optional[str] = None,
69
69
  cache_dir: Optional[Path] = None,
70
+ branch: Optional[str] = None,
70
71
  ) -> LockEntry:
71
- """Move an existing addon's pin to a new version/commit (or its branch HEAD)."""
72
+ """Move an existing addon's pin to a new version/commit (or its branch HEAD).
73
+
74
+ ``branch`` switches the branch the entry tracks (e.g. an upstream feature
75
+ branch under test, or back to the mainline once it merged); without a
76
+ version/commit the pin moves to that branch's head.
77
+ """
72
78
  project_dir = Path(project_dir)
73
79
  lock = Lockfile.load(project_dir / "addons.lock")
74
80
  if name not in lock.entries:
75
81
  raise EditError(f"{name}: not in addons.lock (use 'vendor add')")
76
82
  entry = lock.entries[name]
83
+ if branch:
84
+ entry.branch = branch
77
85
  sha = _resolve_pin(name, entry.source, version, commit, entry.branch, cache_dir)
78
86
  entry.commit = sha
79
87
  # ``version`` is the tag-intent metadata, and ``vendor check``'s moved-tag
@@ -31,6 +31,9 @@ class LockEntry:
31
31
  commit: str
32
32
  version: Optional[str] = None
33
33
  branch: Optional[str] = None
34
+ # An intentional pin outside the source's mainline (a long-lived fork or
35
+ # feature branch): exempt from ``vendor check --mainline``.
36
+ allow_unmerged: bool = False
34
37
 
35
38
  def to_dict(self) -> dict:
36
39
  d: dict = {"source": self.source}
@@ -39,6 +42,8 @@ class LockEntry:
39
42
  if self.branch is not None:
40
43
  d["branch"] = self.branch
41
44
  d["commit"] = self.commit
45
+ if self.allow_unmerged:
46
+ d["allow_unmerged"] = True
42
47
  return d
43
48
 
44
49
  @classmethod
@@ -59,6 +64,7 @@ class LockEntry:
59
64
  commit=str(commit),
60
65
  version=(str(data["version"]) if data.get("version") is not None else None),
61
66
  branch=(str(data["branch"]) if data.get("branch") is not None else None),
67
+ allow_unmerged=bool(data.get("allow_unmerged", False)),
62
68
  )
63
69
 
64
70
 
@@ -41,11 +41,15 @@ def _has_commit(repo: Path, commit: str) -> bool:
41
41
  )
42
42
 
43
43
 
44
+ def _is_local(source: str) -> bool:
45
+ local = Path(source).expanduser()
46
+ return local.is_dir() and (local / ".git").exists()
47
+
48
+
44
49
  def _ensure_clone(source: str, cache_dir: Path | None = None) -> Path:
45
50
  """Return a local repo path for ``source`` (clone into cache if remote), fetched."""
46
- local = Path(source).expanduser()
47
- if local.is_dir() and (local / ".git").exists():
48
- return local
51
+ if _is_local(source):
52
+ return Path(source).expanduser()
49
53
  cache_dir = cache_dir or default_cache_dir()
50
54
  repo = cache_dir / _cache_key(source)
51
55
  if not repo.exists():
@@ -67,7 +71,14 @@ def _ensure_clone(source: str, cache_dir: Path | None = None) -> Path:
67
71
  def resolve_commit(source: str, ref: str, cache_dir: Path | None = None) -> str:
68
72
  """Resolve a ref (tag, branch, or sha) in ``source`` to a full commit sha."""
69
73
  repo = _ensure_clone(source, cache_dir)
70
- for candidate in (ref, f"origin/{ref}", f"refs/tags/{ref}"):
74
+ # A cached clone's local branches are frozen at clone time; fetch only moves
75
+ # origin/*, so the remote-tracking ref must win there. A local source is the
76
+ # user's own repo, where the local branch is the truth.
77
+ if _is_local(source):
78
+ candidates = (ref, f"origin/{ref}", f"refs/tags/{ref}")
79
+ else:
80
+ candidates = (f"origin/{ref}", ref, f"refs/tags/{ref}")
81
+ for candidate in candidates:
71
82
  res = subprocess.run(
72
83
  [
73
84
  "git",
@@ -126,6 +137,37 @@ def get_source_at(
126
137
  return repo
127
138
 
128
139
 
140
+ def commit_on_branch(repo: Path, source: str, commit: str, branch: str) -> bool:
141
+ """True if ``commit`` is in the history of ``branch`` of ``source``.
142
+
143
+ A remote source's cache is fetched for ``branch`` first, so a merge made
144
+ upstream after the cache was filled is seen. Raises RuntimeError when the
145
+ branch does not exist.
146
+ """
147
+ if not _is_local(source):
148
+ subprocess.run(
149
+ ["git", "-C", str(repo), "fetch", "origin",
150
+ f"+refs/heads/{branch}:refs/remotes/origin/{branch}"],
151
+ capture_output=True,
152
+ text=True,
153
+ )
154
+ for ref in (f"refs/remotes/origin/{branch}", f"refs/heads/{branch}"):
155
+ known = subprocess.run(
156
+ ["git", "-C", str(repo), "rev-parse", "--verify", "-q", ref],
157
+ capture_output=True,
158
+ text=True,
159
+ )
160
+ if known.returncode == 0:
161
+ return (
162
+ subprocess.run(
163
+ ["git", "-C", str(repo), "merge-base", "--is-ancestor", commit, ref],
164
+ capture_output=True,
165
+ ).returncode
166
+ == 0
167
+ )
168
+ raise RuntimeError(f"{source}: no branch {branch!r}")
169
+
170
+
129
171
  def tag_resolves_to(repo: Path, tag: str, commit: str) -> bool:
130
172
  """True if ``tag`` in ``repo`` dereferences to ``commit`` (the moved-tag tripwire)."""
131
173
  res = subprocess.run(
@@ -25,7 +25,7 @@ from typing import Optional
25
25
 
26
26
  from odoo_dev.vendor.lock import Lockfile
27
27
  from odoo_dev.vendor.materialize import extract_subtree, is_bytecode, tree_diff
28
- from odoo_dev.vendor.sources import get_source_at, tag_resolves_to
28
+ from odoo_dev.vendor.sources import commit_on_branch, get_source_at, tag_resolves_to
29
29
 
30
30
  _MANIFEST_NAMES = ("__manifest__.py", "__openerp__.py")
31
31
 
@@ -192,12 +192,18 @@ def verify(
192
192
  lock: Lockfile,
193
193
  cache_dir: Optional[Path] = None,
194
194
  allow_hybrid: bool = True,
195
+ mainline: Optional[str] = None,
195
196
  ) -> list:
196
197
  """Return a list of human-readable problems; empty means the gate is green.
197
198
 
198
199
  ``allow_hybrid=False`` (the CI gate for a repo that declares itself fully
199
200
  vendored) additionally fails if any ``addons/`` symlink still points into a
200
201
  ``.repos/`` submodule.
202
+
203
+ ``mainline`` (the production gate, e.g. ``"19.0"``) additionally fails any
204
+ pin whose commit is not in the history of that branch of its source — an
205
+ upstream change still under test on a feature branch. Entries marked
206
+ ``allow_unmerged`` are exempt.
201
207
  """
202
208
  project_dir = Path(project_dir)
203
209
  vendored = project_dir / "vendored"
@@ -236,6 +242,20 @@ def verify(
236
242
  for d in tree_diff(ref, vendored / name):
237
243
  problems.append(f"{name}: vendored copy differs from pin — {d}")
238
244
 
245
+ if mainline and not entry.allow_unmerged:
246
+ try:
247
+ merged = commit_on_branch(repo, entry.source, entry.commit, mainline)
248
+ except Exception as exc:
249
+ problems.append(f"{name}: cannot check mainline {mainline}: {exc}")
250
+ else:
251
+ if not merged:
252
+ problems.append(
253
+ f"{name}: pinned commit {entry.commit[:12]} is not on "
254
+ f"{mainline} of its source (an unmerged upstream change: "
255
+ f"merge it upstream and re-pin, or mark the entry "
256
+ f"'allow_unmerged: true' if the branch pin is intended)"
257
+ )
258
+
239
259
  if entry.version:
240
260
  tag = f"{name}/{entry.version}"
241
261
  if not tag_resolves_to(repo, tag, entry.commit):
@@ -0,0 +1,107 @@
1
+ """Pins on unmerged upstream branches: ``bump --branch`` and the mainline gate.
2
+
3
+ A client may pin a shared addon to an upstream FEATURE branch while the change
4
+ is tested on its staging. Production must only carry commits that reached the
5
+ source's mainline: ``verify(..., mainline="18.0")`` (``vendor check
6
+ --mainline``) fails any pin whose commit is not in the mainline's history,
7
+ unless the entry is marked ``allow_unmerged: true`` (an intentional long-lived
8
+ branch pin).
9
+ """
10
+
11
+ import subprocess
12
+ from pathlib import Path
13
+
14
+ from odoo_dev.vendor.edit import add_addon, bump_addon
15
+ from odoo_dev.vendor.lock import Lockfile, LockEntry
16
+ from odoo_dev.vendor.sync import sync_addons
17
+ from odoo_dev.vendor.verify import verify
18
+
19
+
20
+ def _git(repo: Path, *args: str) -> str:
21
+ return subprocess.run(
22
+ ["git", "-C", str(repo), *args], check=True, capture_output=True, text=True
23
+ ).stdout.strip()
24
+
25
+
26
+ def _source_with_feature(tmp_path: Path):
27
+ """Source repo: mainline ``18.0`` and an unmerged ``feat`` branch."""
28
+ repo = tmp_path / "src"
29
+ (repo / "shared_addon").mkdir(parents=True)
30
+ (repo / "shared_addon" / "__manifest__.py").write_text(
31
+ "{'name': 'shared', 'version': '18.0.1.0.0'}\n"
32
+ )
33
+ (repo / "shared_addon" / "m.py").write_text("v = 1\n")
34
+ _git(repo, "init", "-q", "-b", "18.0")
35
+ _git(repo, "config", "user.email", "t@t")
36
+ _git(repo, "config", "user.name", "t")
37
+ _git(repo, "add", "-A")
38
+ _git(repo, "commit", "-qm", "v1")
39
+ main_sha = _git(repo, "rev-parse", "HEAD")
40
+ _git(repo, "checkout", "-qb", "feat")
41
+ (repo / "shared_addon" / "m.py").write_text("v = 2\n")
42
+ _git(repo, "commit", "-qam", "feature")
43
+ feat_sha = _git(repo, "rev-parse", "HEAD")
44
+ _git(repo, "checkout", "-q", "18.0")
45
+ return repo, main_sha, feat_sha
46
+
47
+
48
+ def _pinned(tmp_path, repo, sha, **kw):
49
+ proj = tmp_path / "client"
50
+ proj.mkdir(exist_ok=True)
51
+ lock = Lockfile(entries={"shared_addon": LockEntry("shared_addon", str(repo), sha, **kw)})
52
+ lock.dump(proj / "addons.lock")
53
+ sync_addons(proj, lock, cache_dir=tmp_path / "cache")
54
+ return proj, lock
55
+
56
+
57
+ def test_mainline_gate_refuses_unmerged_pin(tmp_path):
58
+ repo, _main, feat = _source_with_feature(tmp_path)
59
+ proj, lock = _pinned(tmp_path, repo, feat, branch="feat")
60
+ problems = verify(proj, lock, cache_dir=tmp_path / "cache", mainline="18.0")
61
+ assert len(problems) == 1
62
+ assert "not on 18.0" in problems[0]
63
+
64
+
65
+ def test_mainline_gate_passes_once_merged(tmp_path):
66
+ repo, _main, feat = _source_with_feature(tmp_path)
67
+ _git(repo, "merge", "--no-ff", "-q", "-m", "merge feat", "feat")
68
+ proj, lock = _pinned(tmp_path, repo, feat, branch="feat")
69
+ assert verify(proj, lock, cache_dir=tmp_path / "cache", mainline="18.0") == []
70
+
71
+
72
+ def test_allow_unmerged_opts_out_and_round_trips(tmp_path):
73
+ repo, _main, feat = _source_with_feature(tmp_path)
74
+ proj, lock = _pinned(tmp_path, repo, feat, branch="feat", allow_unmerged=True)
75
+ assert verify(proj, lock, cache_dir=tmp_path / "cache", mainline="18.0") == []
76
+ reloaded = Lockfile.load(proj / "addons.lock")
77
+ assert reloaded.entries["shared_addon"].allow_unmerged is True
78
+ assert "allow_unmerged" in (proj / "addons.lock").read_text()
79
+
80
+
81
+ def test_mainline_pin_passes_and_flag_is_omitted_by_default(tmp_path):
82
+ repo, main, _feat = _source_with_feature(tmp_path)
83
+ proj, lock = _pinned(tmp_path, repo, main, branch="18.0")
84
+ assert verify(proj, lock, cache_dir=tmp_path / "cache", mainline="18.0") == []
85
+ assert "allow_unmerged" not in (proj / "addons.lock").read_text()
86
+
87
+
88
+ def test_without_mainline_a_branch_pin_is_fine(tmp_path):
89
+ repo, _main, feat = _source_with_feature(tmp_path)
90
+ proj, lock = _pinned(tmp_path, repo, feat, branch="feat")
91
+ assert verify(proj, lock, cache_dir=tmp_path / "cache") == []
92
+
93
+
94
+ def test_bump_to_a_branch_tracks_it(tmp_path):
95
+ repo, main, feat = _source_with_feature(tmp_path)
96
+ proj = tmp_path / "client"
97
+ proj.mkdir()
98
+ add_addon(proj, "shared_addon", str(repo), commit=main, cache_dir=tmp_path / "cache")
99
+ entry = bump_addon(proj, "shared_addon", branch="feat", cache_dir=tmp_path / "cache")
100
+ assert entry.branch == "feat"
101
+ assert entry.commit == feat
102
+ assert entry.version is None
103
+ assert (proj / "vendored" / "shared_addon" / "m.py").read_text() == "v = 2\n"
104
+ # an explicit commit wins over the branch head, and the branch is recorded
105
+ entry = bump_addon(proj, "shared_addon", commit=main, branch="18.0",
106
+ cache_dir=tmp_path / "cache")
107
+ assert (entry.branch, entry.commit) == ("18.0", main)
@@ -0,0 +1,58 @@
1
+ """Tests for resolving refs against a source repo (``vendor/sources.py``)."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from odoo_dev.vendor.sources import resolve_commit
7
+
8
+
9
+ def _git(repo: Path, *args: str) -> str:
10
+ return subprocess.run(
11
+ ["git", "-C", str(repo), *args], check=True, capture_output=True, text=True
12
+ ).stdout.strip()
13
+
14
+
15
+ def _commit(repo: Path, content: str) -> str:
16
+ (repo / "m.py").write_text(content)
17
+ _git(repo, "add", "-A")
18
+ _git(repo, "commit", "-qm", content)
19
+ return _git(repo, "rev-parse", "HEAD")
20
+
21
+
22
+ def _upstream(tmp_path: Path) -> Path:
23
+ repo = tmp_path / "upstream"
24
+ repo.mkdir()
25
+ _git(repo, "init", "-q", "-b", "18.0")
26
+ _git(repo, "config", "user.email", "t@t")
27
+ _git(repo, "config", "user.name", "t")
28
+ _commit(repo, "v = 1\n")
29
+ return repo
30
+
31
+
32
+ def test_branch_resolves_to_fetched_tip_not_clone_time_local_branch(tmp_path):
33
+ """A cached clone keeps a local branch frozen at clone time; fetch only moves
34
+ ``origin/<branch>``. Resolving the branch must follow the remote, or every
35
+ ``add``/``bump --branch`` silently pins the tip as of the first clone."""
36
+ upstream = _upstream(tmp_path)
37
+ source = f"file://{upstream}"
38
+ cache = tmp_path / "cache"
39
+ resolve_commit(source, "18.0", cache) # first use: clones into the cache
40
+
41
+ new_tip = _commit(upstream, "v = 2\n")
42
+
43
+ assert resolve_commit(source, "18.0", cache) == new_tip
44
+
45
+
46
+ def test_local_source_resolves_its_own_branch(tmp_path):
47
+ """A local-path source is the user's repo, used as-is: its local branch is
48
+ the truth, even when it also has a (stale) ``origin/<branch>``."""
49
+ upstream = _upstream(tmp_path)
50
+ local = tmp_path / "local"
51
+ subprocess.run(
52
+ ["git", "clone", "-q", str(upstream), str(local)], check=True, capture_output=True
53
+ )
54
+ _git(local, "config", "user.email", "t@t")
55
+ _git(local, "config", "user.name", "t")
56
+ local_tip = _commit(local, "v = local\n")
57
+
58
+ assert resolve_commit(str(local), "18.0") == local_tip
@@ -1,3 +0,0 @@
1
- {
2
- ".": "1.5.0"
3
- }
@@ -1,3 +0,0 @@
1
- """Odoo Development Environment Helper."""
2
-
3
- __version__ = "1.5.0" # x-release-please-version
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