git-private2public 0.2.3__tar.gz → 0.2.4__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,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.4
4
+
5
+ - Scan preserved public release tags before publishing and retain only their redacted pre-existing findings.
6
+ - Independently reject any new scan findings after publication while keeping unchanged tag refs intact.
7
+
3
8
  ## 0.2.3
4
9
 
5
10
  - Preserve existing public target tags unchanged during snapshot publication when explicitly enabled.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: git-private2public
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Like .gitignore, but for what goes public. Keep a sanitized public mirror of your private repo.
5
5
  Project-URL: Homepage, https://github.com/megamen32/git-private2public
6
6
  Project-URL: Repository, https://github.com/megamen32/git-private2public
@@ -35,7 +35,7 @@ from pathlib import Path
35
35
  import time
36
36
  from typing import Iterable
37
37
 
38
- __version__ = "0.2.3"
38
+ __version__ = "0.2.4"
39
39
 
40
40
  try:
41
41
  import yaml
@@ -898,6 +898,35 @@ def _verify_target_refs_exact(url: str, expected_refs: dict[str, str]) -> None:
898
898
  raise SystemExit(f"post-push target refs differ: {'; '.join(details)}")
899
899
 
900
900
 
901
+ def _target_tag_scan_refs(repo: Path, tags: dict[str, str]) -> list[str]:
902
+ """Fetch preserved tags into an isolated namespace and return exact refs."""
903
+ if not tags:
904
+ return []
905
+ run(
906
+ ["git", "fetch", "--quiet", "--no-tags", "target", "+refs/tags/*:refs/target-tags/*"],
907
+ cwd=str(repo),
908
+ )
909
+ lines = run(
910
+ ["git", "for-each-ref", "--format=%(objectname) %(refname)", "refs/target-tags/"],
911
+ cwd=str(repo),
912
+ ).stdout.splitlines()
913
+ actual = {}
914
+ refs = []
915
+ for line in lines:
916
+ object_id, ref_name = line.split(maxsplit=1)
917
+ remote_ref = "refs/tags/" + ref_name.removeprefix("refs/target-tags/")
918
+ actual[remote_ref] = object_id
919
+ refs.append(ref_name)
920
+ if actual != tags:
921
+ raise SystemExit("public target tags changed while their baseline was scanned")
922
+ return sorted(refs)
923
+
924
+
925
+ def _new_violations(current: list[str], preserved_baseline: list[str]) -> list[str]:
926
+ """Find new findings while retaining unchanged historical public tags."""
927
+ return sorted(set(current) - set(preserved_baseline))
928
+
929
+
901
930
  def _local_publish_refs(work: Path, config: Config, head: str) -> dict[str, str]:
902
931
  """Build the exact remote ref set that a successful publish must expose."""
903
932
  expected = {f"refs/heads/{branch}": head for branch in config.push_branches}
@@ -1138,11 +1167,34 @@ def publish(config: Config, scan_only: bool = False, cwd: str | None = None, out
1138
1167
  preserve_target_tags=config.preserve_target_tags,
1139
1168
  )
1140
1169
  _verify_target_tags_unchanged(expected_target_tags, preserved_target_tags)
1170
+ run(["git", "remote", "add", "target", target_url], cwd=str(work))
1171
+ tag_refs = _target_tag_scan_refs(work, preserved_target_tags)
1172
+ try:
1173
+ preserved_tag_findings = scan_history(
1174
+ work,
1175
+ effective_scan_rules(config.fail_on_match),
1176
+ config.allow_domains,
1177
+ refs=tag_refs,
1178
+ scan_ignore=config.scan_ignore,
1179
+ )
1180
+ except ScanError as exc:
1181
+ raise SystemExit(f"preserved public tag scan incomplete: {exc}") from exc
1182
+ if preserved_tag_findings:
1183
+ print(
1184
+ f"ℹ Preserving {len(preserved_target_tags)} existing public tag(s) with "
1185
+ f"{len(preserved_tag_findings)} pre-existing scan finding(s); checking that "
1186
+ "the publication adds none.",
1187
+ file=sys.stderr,
1188
+ )
1189
+ latest_target_tags = {
1190
+ ref: sha for ref, sha in _remote_refs(target_url).items()
1191
+ if ref.startswith("refs/tags/")
1192
+ }
1193
+ _verify_target_tags_unchanged(expected_target_tags, latest_target_tags)
1141
1194
  expected_public_refs.update(preserved_target_tags)
1142
1195
 
1143
1196
  # Push to target
1144
1197
  print(f"▸ Pushing to {mask_url(target_url)} ...", file=sys.stderr)
1145
- run(["git", "remote", "add", "target", target_url], cwd=str(work))
1146
1198
 
1147
1199
  if config.preserve_target_tags:
1148
1200
  print(
@@ -1194,8 +1246,23 @@ def publish(config: Config, scan_only: bool = False, cwd: str | None = None, out
1194
1246
  )
1195
1247
  except ScanError as exc:
1196
1248
  raise SystemExit(f"post-push target scan incomplete: {exc}") from exc
1197
- if verify_violations:
1198
- raise SystemExit("post-push target scan found violations")
1249
+ new_findings = _new_violations(verify_violations, preserved_tag_findings)
1250
+ if new_findings:
1251
+ print(
1252
+ f"✗ Post-push scan found {len(new_findings)} new violation(s):",
1253
+ file=sys.stderr,
1254
+ )
1255
+ for finding in new_findings[:30]:
1256
+ print(f" {finding}", file=sys.stderr)
1257
+ if len(new_findings) > 30:
1258
+ print(f" ... and {len(new_findings) - 30} more", file=sys.stderr)
1259
+ raise SystemExit("post-push target scan found new violations")
1260
+ if preserved_tag_findings:
1261
+ print(
1262
+ f"✓ Published tree adds no findings; {len(preserved_tag_findings)} "
1263
+ "pre-existing finding(s) remain only through unchanged public history.",
1264
+ file=sys.stderr,
1265
+ )
1199
1266
  print("✓ Published target independently re-scanned.", file=sys.stderr)
1200
1267
  else:
1201
1268
  print(
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "git-private2public"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Like .gitignore, but for what goes public. Keep a sanitized public mirror of your private repo."
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -185,8 +185,8 @@ def test_replace_rule_strips_separator_whitespace():
185
185
 
186
186
 
187
187
  def test_version_constant_matches_package_version():
188
- assert g.__version__ == "0.2.3"
189
- assert 'version = "0.2.3"' in (Path(__file__).parent.parent / "pyproject.toml").read_text()
188
+ assert g.__version__ == "0.2.4"
189
+ assert 'version = "0.2.4"' in (Path(__file__).parent.parent / "pyproject.toml").read_text()
190
190
 
191
191
 
192
192
  def test_gitpublic_secret_check_finds_only_replace_and_scan(tmp_path: Path):
@@ -1064,7 +1064,7 @@ def test_snapshot_prunes_secret_in_removed_private_history_before_scan(tmp_path:
1064
1064
  assert public_files == ["safe.txt"]
1065
1065
 
1066
1066
 
1067
- def test_snapshot_publish_preserves_existing_public_release_tags(tmp_path: Path):
1067
+ def test_snapshot_publish_preserves_existing_public_release_tags_and_finds_no_new_leaks(tmp_path: Path, capfd):
1068
1068
  source = tmp_path / "private"
1069
1069
  seed = tmp_path / "seed"
1070
1070
  target = tmp_path / "public.git"
@@ -1081,7 +1081,9 @@ def test_snapshot_publish_preserves_existing_public_release_tags(tmp_path: Path)
1081
1081
  subprocess.run(["git", "-C", str(repo), "config", "user.name", name], check=True)
1082
1082
  subprocess.run(["git", "-C", str(repo), "config", "user.email", email], check=True)
1083
1083
 
1084
- (seed / "public.txt").write_text("previous release\n")
1084
+ # A previously published dummy credential-shaped fixture remains reachable
1085
+ # only through immutable release tags. Its source and value stay redacted.
1086
+ (seed / "public.txt").write_text("previous release " + "sk-" + "x" * 20 + "\n")
1085
1087
  subprocess.run(["git", "-C", str(seed), "add", "public.txt"], check=True)
1086
1088
  subprocess.run(["git", "-C", str(seed), "commit", "-q", "-m", "previous public snapshot"], check=True)
1087
1089
  subprocess.run(["git", "-C", str(seed), "tag", "node-v1024"], check=True)
@@ -1104,6 +1106,9 @@ def test_snapshot_publish_preserves_existing_public_release_tags(tmp_path: Path)
1104
1106
  push_force=True, preserve_target_tags=True,
1105
1107
  )
1106
1108
  assert g.publish(cfg, cwd=str(source)) == 0
1109
+ publish_output = capfd.readouterr().err
1110
+ assert "pre-existing scan finding(s)" in publish_output
1111
+ assert "publication adds none" in publish_output
1107
1112
 
1108
1113
  after_tags = subprocess.run(
1109
1114
  ["git", "--git-dir", str(target), "for-each-ref", "--format=%(objectname) %(refname)", "refs/tags/"],