multicz 0.2.0__tar.gz → 0.2.2__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.2
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.2"
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 = [
@@ -0,0 +1,3 @@
1
+ from .cli import app
2
+
3
+ app()
@@ -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,12 +1035,20 @@ 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)
1038
1048
  if not args:
1039
1049
  return
1040
- console.print(f" [dim]post_bump:[/] {command}")
1050
+ # stderr, so `multicz bump --output json | jq` stays parseable.
1051
+ err.print(f" [dim]post_bump:[/] {command}")
1041
1052
  result = subprocess.run(
1042
1053
  args, cwd=repo, capture_output=True, text=True
1043
1054
  )
@@ -1424,21 +1435,37 @@ def bump(
1424
1435
  # changelog, state) so commands like `uv lock`, `npm install
1425
1436
  # --package-lock-only`, `cargo update --workspace`, `helm dependency
1426
1437
  # 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.
1438
+ # Cargo.toml. Files modified by hooks are auto-detected and folded
1439
+ # into ``written`` so they ride the release commit.
1440
+ #
1441
+ # Detection compares content hashes — not just the dirty-paths set —
1442
+ # because the entry point is typically ``uv run multicz bump``, and
1443
+ # ``uv run`` re-syncs the venv (which can rewrite ``uv.lock``) before
1444
+ # multicz code runs at all. By the time we snapshot, uv.lock is
1445
+ # already in the dirty set; a set diff would miss the *second*
1446
+ # rewrite the post_bump hook performs against the new pyproject. The
1447
+ # hash comparison catches it.
1431
1448
  if not dry_run and applied:
1432
1449
  hook_components = [
1433
1450
  n for n in applied if config.components[n].post_bump
1434
1451
  ]
1435
1452
  if hook_components:
1436
1453
  before_dirty = _porcelain_paths(repo)
1454
+ before_hashes: dict[str, str | None] = {
1455
+ relpath: _hash_file(repo / relpath)
1456
+ for relpath in before_dirty
1457
+ }
1437
1458
  for name in hook_components:
1438
1459
  for command in config.components[name].post_bump:
1439
1460
  _run_post_bump_hook(repo, command)
1440
1461
  after_dirty = _porcelain_paths(repo)
1441
- for relpath in sorted(after_dirty - before_dirty):
1462
+ hook_modified: set[str] = {
1463
+ relpath
1464
+ for relpath in after_dirty
1465
+ if relpath not in before_dirty
1466
+ or _hash_file(repo / relpath) != before_hashes.get(relpath)
1467
+ }
1468
+ for relpath in sorted(hook_modified):
1442
1469
  path = (repo / relpath).resolve()
1443
1470
  if path.is_file() and path not in written:
1444
1471
  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