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.
- {git_private2public-0.2.3 → git_private2public-0.2.4}/CHANGELOG.md +5 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/PKG-INFO +1 -1
- {git_private2public-0.2.3 → git_private2public-0.2.4}/git_private2public.py +71 -4
- {git_private2public-0.2.3 → git_private2public-0.2.4}/pyproject.toml +1 -1
- {git_private2public-0.2.3 → git_private2public-0.2.4}/tests/test_git_private2public.py +9 -4
- {git_private2public-0.2.3 → git_private2public-0.2.4}/.github/workflows/ci.yml +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/.github/workflows/release.yml +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/.gitignore +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/LICENSE +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/README.md +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/README.ru.md +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/README.zh.md +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/docs/ADVANCED.md +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/docs/ADVANCED.ru.md +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/docs/VERIFICATION.md +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/docs/assets/readme-hero.png +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/example.yaml +0 -0
- {git_private2public-0.2.3 → git_private2public-0.2.4}/templates/publish.yml +0 -0
|
@@ -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
|
+
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.
|
|
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
|
-
|
|
1198
|
-
|
|
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(
|
|
@@ -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.
|
|
189
|
-
assert 'version = "0.2.
|
|
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
|
|
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
|
-
|
|
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/"],
|
|
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
|
|
File without changes
|