multicz 0.2.0__tar.gz → 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: multicz
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Multi-component versioning for monorepos: bump apps, charts, and images independently from conventional commits.
5
5
  Keywords: semver,monorepo,helm,conventional-commits,release,versioning
6
6
  Author: Chris
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "multicz"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Multi-component versioning for monorepos: bump apps, charts, and images independently from conventional commits."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import hashlib
5
6
  import shlex
6
7
  import subprocess
7
8
  import sys
@@ -1010,9 +1011,11 @@ def _git(repo: Path, *args: str) -> str:
1010
1011
  def _porcelain_paths(repo: Path) -> set[str]:
1011
1012
  """Repo-relative paths currently dirty in the working tree.
1012
1013
 
1013
- Used to diff working-tree state before vs. after running ``post_bump``
1014
- hooks: anything that wasn't dirty before but is dirty after came from
1015
- a hook, and gets pulled into the release commit.
1014
+ Used to identify candidate paths to hash before/after running
1015
+ ``post_bump`` hooks. A pure set diff would miss a file that's
1016
+ dirty both before and after with different content the
1017
+ canonical case being ``uv run`` itself silently re-syncing
1018
+ ``uv.lock`` before multicz even gets to run.
1016
1019
  """
1017
1020
  out = subprocess.run(
1018
1021
  ["git", "status", "--porcelain"],
@@ -1032,6 +1035,13 @@ def _porcelain_paths(repo: Path) -> set[str]:
1032
1035
  return paths
1033
1036
 
1034
1037
 
1038
+ def _hash_file(path: Path) -> str | None:
1039
+ try:
1040
+ return hashlib.sha256(path.read_bytes()).hexdigest()
1041
+ except OSError:
1042
+ return None
1043
+
1044
+
1035
1045
  def _run_post_bump_hook(repo: Path, command: str) -> None:
1036
1046
  """Execute a single ``post_bump`` shell command in ``repo``."""
1037
1047
  args = shlex.split(command)
@@ -1424,21 +1434,37 @@ def bump(
1424
1434
  # changelog, state) so commands like `uv lock`, `npm install
1425
1435
  # --package-lock-only`, `cargo update --workspace`, `helm dependency
1426
1436
  # update` see the new pyproject.toml / package.json / Chart.yaml /
1427
- # Cargo.toml. Files modified by hooks are auto-detected via the git
1428
- # status diff and folded into ``written`` so they ride the release
1429
- # commit. Hooks only run when ``not dry_run`` — same gate as the
1430
- # writes themselves.
1437
+ # Cargo.toml. Files modified by hooks are auto-detected and folded
1438
+ # into ``written`` so they ride the release commit.
1439
+ #
1440
+ # Detection compares content hashes — not just the dirty-paths set —
1441
+ # because the entry point is typically ``uv run multicz bump``, and
1442
+ # ``uv run`` re-syncs the venv (which can rewrite ``uv.lock``) before
1443
+ # multicz code runs at all. By the time we snapshot, uv.lock is
1444
+ # already in the dirty set; a set diff would miss the *second*
1445
+ # rewrite the post_bump hook performs against the new pyproject. The
1446
+ # hash comparison catches it.
1431
1447
  if not dry_run and applied:
1432
1448
  hook_components = [
1433
1449
  n for n in applied if config.components[n].post_bump
1434
1450
  ]
1435
1451
  if hook_components:
1436
1452
  before_dirty = _porcelain_paths(repo)
1453
+ before_hashes: dict[str, str | None] = {
1454
+ relpath: _hash_file(repo / relpath)
1455
+ for relpath in before_dirty
1456
+ }
1437
1457
  for name in hook_components:
1438
1458
  for command in config.components[name].post_bump:
1439
1459
  _run_post_bump_hook(repo, command)
1440
1460
  after_dirty = _porcelain_paths(repo)
1441
- for relpath in sorted(after_dirty - before_dirty):
1461
+ hook_modified: set[str] = {
1462
+ relpath
1463
+ for relpath in after_dirty
1464
+ if relpath not in before_dirty
1465
+ or _hash_file(repo / relpath) != before_hashes.get(relpath)
1466
+ }
1467
+ for relpath in sorted(hook_modified):
1442
1468
  path = (repo / relpath).resolve()
1443
1469
  if path.is_file() and path not in written:
1444
1470
  written.append(path)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes