@@ -64,7 +64,7 @@ def _git_head_commit(checkout: Path) -> str:
6464
6565
6666def _checkout_clean (checkout : Path ) -> str | None :
67- """Return a description of any tracked difference from HEAD, else None."""
67+ """Return a description of any non-ignored difference from HEAD, else None."""
6868 result = subprocess .run (
6969 ["git" , "-C" , str (checkout ), "status" , "--porcelain" ],
7070 capture_output = True ,
@@ -78,6 +78,20 @@ def _checkout_clean(checkout: Path) -> str | None:
7878 return None
7979
8080
81+ def _prepare_workspace (workspace : Path ) -> None :
82+ """Create an empty gate workspace or reject stale caller-owned state."""
83+ if workspace .exists ():
84+ if not workspace .is_dir ():
85+ raise SystemExit (f"FAIL: release-gate workspace is not a directory: { workspace } " )
86+ if any (workspace .iterdir ()):
87+ raise SystemExit (
88+ "FAIL: --work-dir must be absent or empty; refusing to reuse "
89+ f"stale release-gate state under { workspace } "
90+ )
91+ else :
92+ workspace .mkdir (parents = True )
93+
94+
8195def _extract_head_archive (checkout : Path , target : Path ) -> int :
8296 """Extract `git archive HEAD` into target; return the file count."""
8397 archive = subprocess .run (
@@ -96,7 +110,7 @@ def _extract_head_archive(checkout: Path, target: Path) -> int:
96110
97111
98112def _snapshot_matches_head (checkout : Path , srctree : Path ) -> list [str ]:
99- """Prove every snapshot file is byte- identical to HEAD's blob ."""
113+ """Prove the snapshot file set and contents are identical to HEAD."""
100114 tree = subprocess .run (
101115 ["git" , "-C" , str (checkout ), "ls-tree" , "-r" , "HEAD" ],
102116 capture_output = True ,
@@ -112,21 +126,29 @@ def _snapshot_matches_head(checkout: Path, srctree: Path) -> list[str]:
112126 for entry in tree .stdout .splitlines ()
113127 )
114128 }
129+ actual_names = {
130+ path .relative_to (srctree ).as_posix ()
131+ for path in srctree .rglob ("*" )
132+ if path .is_file () or path .is_symlink ()
133+ }
134+ expected_names = set (expected )
135+ mismatches = expected_names .symmetric_difference (actual_names )
136+ common_names = sorted (expected_names & actual_names )
115137 hashed = subprocess .run (
116138 ["git" , "-C" , str (checkout ), "hash-object" , "--stdin-paths" ],
117- input = "" .join (str (srctree / name ) + "\n " for name in expected ),
139+ input = "" .join (str (srctree / name ) + "\n " for name in common_names ),
118140 capture_output = True ,
119141 text = True ,
120142 )
121143 if hashed .returncode != 0 :
122144 raise SystemExit ("FAIL: git hash-object --stdin-paths failed" )
123145 actual = hashed .stdout .splitlines ()
124- mismatches = [
146+ mismatches . update (
125147 name
126- for name , blob in zip (expected , actual )
148+ for name , blob in zip (common_names , actual )
127149 if blob != expected [name ]
128- ]
129- return mismatches
150+ )
151+ return sorted ( mismatches )
130152
131153
132154def _copy_snapshot_subtree (srctree : Path , rel_dir : str , target : Path ) -> int :
@@ -240,7 +262,7 @@ def main(argv: list[str] | None = None) -> int:
240262 if owned_workspace
241263 else args .work_dir .resolve ()
242264 )
243- workspace . mkdir ( parents = True , exist_ok = True )
265+ _prepare_workspace ( workspace )
244266 print (f"workspace: { workspace } " )
245267
246268 srctree = workspace / "srctree"
0 commit comments