gitvow 0.28.2__tar.gz → 0.28.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.
- {gitvow-0.28.2/src/gitvow.egg-info → gitvow-0.28.4}/PKG-INFO +1 -1
- {gitvow-0.28.2 → gitvow-0.28.4}/pyproject.toml +1 -1
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/default_policy.json +24 -4
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/hooks/__init__.py +14 -9
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/install.py +60 -10
- {gitvow-0.28.2 → gitvow-0.28.4/src/gitvow.egg-info}/PKG-INFO +1 -1
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow.egg-info/SOURCES.txt +2 -0
- gitvow-0.28.4/tests/test_answered_confirm_commit.py +35 -0
- gitvow-0.28.4/tests/test_policy_refresh.py +58 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/LICENSE +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/README.md +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/setup.cfg +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/__init__.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/__main__.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/adapters.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/claims.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/cli.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/collect.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/decisions.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/digest.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/export.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/pack.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/paths.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/policy.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/pricing.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/proposals.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/providers.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/recall.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/redact.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/report.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/rules.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/safewrite.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/scan.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/selftest.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/serve.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/snapshots.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/state.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/status.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/sync.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/targets.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/transcript.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow.egg-info/dependency_links.txt +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow.egg-info/entry_points.txt +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow.egg-info/requires.txt +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow.egg-info/top_level.txt +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_adapters.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_claims.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_collect.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_command_position.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_commit_window.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_cost.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_decisions.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_digest.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_e2e_globalopt.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_export.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_external_adapter.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_git_rules.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_hardening.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_hooks.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_install_cli.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_pack.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_packaging.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_policy.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_proposals.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_providers.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_recall.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_redact.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_report.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_rules.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_scan.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_serve.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_snapshots.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_status.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_sync.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_sync_http.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_targets.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_tiers.py +0 -0
- {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_transcript_formats.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitvow
|
|
3
|
-
Version: 0.28.
|
|
3
|
+
Version: 0.28.4
|
|
4
4
|
Summary: Provenance and policy gate for AI-agent coding sessions: session trailers on commits, redacted session notes in git, a tool-call gate, and a local ledger. No runtime dependencies.
|
|
5
5
|
Author-email: Nikhil Bora <nikhil@wirevow.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitvow"
|
|
7
|
-
version = "0.28.
|
|
7
|
+
version = "0.28.4"
|
|
8
8
|
description = "Provenance and policy gate for AI-agent coding sessions: session trailers on commits, redacted session notes in git, a tool-call gate, and a local ledger. No runtime dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -80,8 +80,7 @@
|
|
|
80
80
|
"patch",
|
|
81
81
|
"rollout",
|
|
82
82
|
"set",
|
|
83
|
-
"scale"
|
|
84
|
-
"exec"
|
|
83
|
+
"scale"
|
|
85
84
|
],
|
|
86
85
|
"target": "(?i)prod|live",
|
|
87
86
|
"reason": "cluster mutation in production"
|
|
@@ -95,12 +94,33 @@
|
|
|
95
94
|
"patch",
|
|
96
95
|
"rollout",
|
|
97
96
|
"set",
|
|
98
|
-
"scale"
|
|
99
|
-
"exec"
|
|
97
|
+
"scale"
|
|
100
98
|
],
|
|
101
99
|
"reason": "cluster mutation",
|
|
102
100
|
"when": "observe"
|
|
103
101
|
},
|
|
102
|
+
{
|
|
103
|
+
"program": "kubectl",
|
|
104
|
+
"verbs": [
|
|
105
|
+
"exec",
|
|
106
|
+
"port-forward",
|
|
107
|
+
"cp",
|
|
108
|
+
"debug"
|
|
109
|
+
],
|
|
110
|
+
"target": "(?i)prod|live",
|
|
111
|
+
"reason": "exec into a production workload"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"program": "kubectl",
|
|
115
|
+
"verbs": [
|
|
116
|
+
"exec",
|
|
117
|
+
"port-forward",
|
|
118
|
+
"cp",
|
|
119
|
+
"debug"
|
|
120
|
+
],
|
|
121
|
+
"reason": "exec into a cluster workload",
|
|
122
|
+
"when": "observe"
|
|
123
|
+
},
|
|
104
124
|
{
|
|
105
125
|
"program": "helm",
|
|
106
126
|
"verbs": [
|
|
@@ -186,17 +186,22 @@ def pre_tool_use(h: dict[str, Any], home: str | None = None) -> tuple[int, str]:
|
|
|
186
186
|
f = dict(d.findings[0], immediate=True)
|
|
187
187
|
text = redact(f["finding"], rules) if rules is not None else f["finding"]
|
|
188
188
|
prior = dec.session_answer(cwd, text)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if
|
|
189
|
+
answered = bool(
|
|
190
|
+
session_scope and prior and prior.get("answer") == "accepted" and prior.get("scope") == "session"
|
|
191
|
+
)
|
|
192
|
+
if answered:
|
|
193
|
+
log_event(cwd, "allowed_by_session_answer", {**payload, "by": prior.get("by")}) # type: ignore[union-attr]
|
|
194
|
+
# Do not return yet: the same command may also commit (`git commit && git push`), and until 0.28.4 the
|
|
195
|
+
# early return here skipped the commit bookkeeping below, so the commit lost its session trailer.
|
|
196
|
+
elif prior and prior.get("answer") == "declined":
|
|
193
197
|
log_event(cwd, "confirm_required", {**payload, "declined_by": prior.get("by")})
|
|
194
198
|
return 2, f"BLOCKED: {text} was declined by {prior.get('by')} this session ({d.reason})."
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
199
|
+
if not answered:
|
|
200
|
+
st = load_state(cwd)
|
|
201
|
+
dec.add(cwd, [f], st.get("steps", 0) + 1, tool, rules)
|
|
202
|
+
n = next((x["n"] for x in dec.open_findings(cwd) if x["finding"] == text), None)
|
|
203
|
+
log_event(cwd, "confirm_required", {**payload, "finding": n})
|
|
204
|
+
return 2, confirm_message(d, n, session_scope)
|
|
200
205
|
if d.deferred or d.observed:
|
|
201
206
|
st = load_state(cwd)
|
|
202
207
|
new = dec.add(cwd, list(d.findings), st.get("steps", 0) + 1, tool, rules)
|
|
@@ -510,17 +510,70 @@ def _write_git_hook(dirpath: str, root: str | None = None) -> str:
|
|
|
510
510
|
return os.path.join(dirpath, "prepare-commit-msg")
|
|
511
511
|
|
|
512
512
|
|
|
513
|
+
def _default_policy_text() -> str:
|
|
514
|
+
from .policy import DEFAULT_POLICY_PATH
|
|
515
|
+
|
|
516
|
+
with open(DEFAULT_POLICY_PATH) as fh:
|
|
517
|
+
return fh.read()
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _body_digest(data: dict[str, Any]) -> str:
|
|
521
|
+
"""Digest of a policy's content with the marker removed, in the form the copy is written in."""
|
|
522
|
+
import hashlib
|
|
523
|
+
|
|
524
|
+
body = {k: v for k, v in data.items() if k != "_from_default"}
|
|
525
|
+
return hashlib.sha256((json.dumps(body, indent=2) + "\n").encode()).hexdigest()
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def stamp_default(text: str) -> str:
|
|
529
|
+
"""The default policy as a copy that knows it is one: `_from_default` is the digest of its own body, so a
|
|
530
|
+
later install can tell an untouched copy (body digest still equals the marker) from one a person edited."""
|
|
531
|
+
data = json.loads(text)
|
|
532
|
+
data["_from_default"] = _body_digest(data)
|
|
533
|
+
return json.dumps(data, indent=2) + "\n"
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def place_policy(pol_path: str, root: str | None = None) -> str | None:
|
|
537
|
+
"""Copy the default policy to `pol_path` when absent, or refresh it when it is an untouched copy of an older
|
|
538
|
+
default. A policy a person edited, or one copied before the marker existed, is never touched.
|
|
539
|
+
|
|
540
|
+
Until 0.28.3 the default was copied once and never again, so a repository kept the rules of the version it
|
|
541
|
+
was installed with: the `git -C … push` fix, the targets and the indirect tier never reached the founder's
|
|
542
|
+
own repositories, and the stale pattern rule stopped an MCP verification that merely mentioned a push.
|
|
543
|
+
"""
|
|
544
|
+
text = _default_policy_text()
|
|
545
|
+
stamped = stamp_default(text)
|
|
546
|
+
if not os.path.exists(pol_path):
|
|
547
|
+
write_if_changed(check_target(pol_path, root), stamped)
|
|
548
|
+
return f"default policy → {pol_path}"
|
|
549
|
+
try:
|
|
550
|
+
with open(pol_path) as fh:
|
|
551
|
+
cur = json.load(fh)
|
|
552
|
+
except (OSError, json.JSONDecodeError):
|
|
553
|
+
return None
|
|
554
|
+
mark = cur.get("_from_default")
|
|
555
|
+
if mark is None:
|
|
556
|
+
return (
|
|
557
|
+
f"policy left as is: {pol_path} predates the marker or was written by hand (compare with `gitvow policy`)"
|
|
558
|
+
)
|
|
559
|
+
if mark != _body_digest(cur):
|
|
560
|
+
return f"policy left as is: {pol_path} was edited after it was copied"
|
|
561
|
+
with open(pol_path) as fh:
|
|
562
|
+
if stamped == fh.read():
|
|
563
|
+
return None
|
|
564
|
+
write_if_changed(check_target(pol_path, root), stamped)
|
|
565
|
+
return f"policy refreshed to the current default → {pol_path} (an untouched copy of an older one)"
|
|
566
|
+
|
|
567
|
+
|
|
513
568
|
def install_user(home: str, cmd_prefix: str | None = None, agent: str = "claude") -> list[str]:
|
|
514
569
|
base = os.path.join(home, ".gitvow")
|
|
515
570
|
os.makedirs(base, exist_ok=True)
|
|
516
571
|
cmd_prefix = cmd_prefix or executable_command()
|
|
517
572
|
done = [f"hook command → {cmd_prefix}"]
|
|
518
573
|
pol = os.path.join(base, "policy.json")
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
shutil.copy(DEFAULT_POLICY_PATH, pol)
|
|
523
|
-
done.append(f"default policy → {pol}")
|
|
574
|
+
placed = place_policy(pol)
|
|
575
|
+
if placed:
|
|
576
|
+
done.append(placed)
|
|
524
577
|
_write_git_hook(os.path.join(base, "git-hooks"))
|
|
525
578
|
ext = None if agent in AGENT_FILES else _external(agent)
|
|
526
579
|
settings_rel = AGENT_FILES[agent][0] if agent in AGENT_FILES else _external_files(ext[1])[0] # type: ignore[index]
|
|
@@ -586,11 +639,8 @@ def _exclude_local_sinks(repo: str) -> None:
|
|
|
586
639
|
def install_repo(repo: str, cmd_prefix: str = "gitvow", agent: str = "claude") -> list[str]:
|
|
587
640
|
base = os.path.join(repo, ".gitvow")
|
|
588
641
|
os.makedirs(base, exist_ok=True)
|
|
589
|
-
from .policy import DEFAULT_POLICY_PATH
|
|
590
|
-
|
|
591
642
|
pol = os.path.join(base, "policy.json")
|
|
592
|
-
|
|
593
|
-
shutil.copy(DEFAULT_POLICY_PATH, pol)
|
|
643
|
+
placed = place_policy(pol, root=repo)
|
|
594
644
|
_write_git_hook(os.path.join(base, "git-hooks"), root=repo)
|
|
595
645
|
ext = None if agent in AGENT_FILES else _external(agent)
|
|
596
646
|
repo_rel = AGENT_FILES[agent][1] if agent in AGENT_FILES else _external_files(ext[1])[1] # type: ignore[index]
|
|
@@ -604,7 +654,7 @@ def install_repo(repo: str, cmd_prefix: str = "gitvow", agent: str = "claude") -
|
|
|
604
654
|
_set_notes_config(repo, [])
|
|
605
655
|
_exclude_local_sinks(repo)
|
|
606
656
|
return [
|
|
607
|
-
f"policy → {pol}",
|
|
657
|
+
placed or f"policy → {pol}",
|
|
608
658
|
f"hooks merged into {repo_rel}",
|
|
609
659
|
"core.hooksPath → .gitvow/git-hooks",
|
|
610
660
|
f"notes.displayRef / notes.rewriteRef → {NOTES_GLOB}",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitvow
|
|
3
|
-
Version: 0.28.
|
|
3
|
+
Version: 0.28.4
|
|
4
4
|
Summary: Provenance and policy gate for AI-agent coding sessions: session trailers on commits, redacted session notes in git, a tool-call gate, and a local ledger. No runtime dependencies.
|
|
5
5
|
Author-email: Nikhil Bora <nikhil@wirevow.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -40,6 +40,7 @@ src/gitvow.egg-info/requires.txt
|
|
|
40
40
|
src/gitvow.egg-info/top_level.txt
|
|
41
41
|
src/gitvow/hooks/__init__.py
|
|
42
42
|
tests/test_adapters.py
|
|
43
|
+
tests/test_answered_confirm_commit.py
|
|
43
44
|
tests/test_claims.py
|
|
44
45
|
tests/test_collect.py
|
|
45
46
|
tests/test_command_position.py
|
|
@@ -57,6 +58,7 @@ tests/test_install_cli.py
|
|
|
57
58
|
tests/test_pack.py
|
|
58
59
|
tests/test_packaging.py
|
|
59
60
|
tests/test_policy.py
|
|
61
|
+
tests/test_policy_refresh.py
|
|
60
62
|
tests/test_proposals.py
|
|
61
63
|
tests/test_providers.py
|
|
62
64
|
tests/test_recall.py
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""0.28.4: a command that both commits and runs a session-answered confirm still gets its commit bookkeeping.
|
|
2
|
+
|
|
3
|
+
Found on the founder's machine: after a push to main had been accepted for the session, `git commit … && git push`
|
|
4
|
+
in one command was allowed by the session answer and returned before the commit flag was set, so the commit carried
|
|
5
|
+
no session trailer. Three of four repositories lost the trailer on the same evening the same way.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from gitvow import decisions as dec
|
|
11
|
+
from gitvow.hooks import pre_tool_use, session_start
|
|
12
|
+
from gitvow.install import _write_git_hook
|
|
13
|
+
from gitvow.policy import load_policy
|
|
14
|
+
from tests.conftest import git
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_session_answered_push_in_the_same_command_as_a_commit_keeps_the_trailer(repo, home, payload):
|
|
18
|
+
_write_git_hook(str(repo / ".gitvow" / "git-hooks"))
|
|
19
|
+
git(repo, "config", "core.hooksPath", ".gitvow/git-hooks")
|
|
20
|
+
git(repo, "remote", "add", "origin", "git@example.com:acme/ledger.git")
|
|
21
|
+
session_start(payload("SessionStart"), str(home))
|
|
22
|
+
cmd = {"command": 'git commit -qm "x" && git push origin main'}
|
|
23
|
+
code, msg = pre_tool_use(payload("PreToolUse", "Bash", cmd), str(home))
|
|
24
|
+
assert code == 2 and "pushing to a protected branch" in msg
|
|
25
|
+
dec.decide(str(repo), "1", "accept", load_policy(str(repo), str(home)), scope="session", reason="ok", rules=[])
|
|
26
|
+
code, msg = pre_tool_use(payload("PreToolUse", "Bash", cmd), str(home))
|
|
27
|
+
assert code == 0
|
|
28
|
+
st = json.loads((repo / ".git" / "gitvow-session.json").read_text())
|
|
29
|
+
assert st.get("pending_commit") and st["steps"] == 1
|
|
30
|
+
(repo / "a.txt").write_text("y\n")
|
|
31
|
+
git(repo, "commit", "-qam", "after the answer")
|
|
32
|
+
body = git(repo, "log", "-1", "--format=%B")
|
|
33
|
+
assert "Gitvow-Session: sess-1" in body and "Gitvow-Accepted: run git (pushing to a protected branch)" in body
|
|
34
|
+
log = (repo / ".git" / "gitvow-hooks.log").read_text()
|
|
35
|
+
assert '"kind": "allowed_by_session_answer"' in log
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""0.28.3: an install refreshes an untouched copy of an older default policy, and leaves an edited one alone."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from gitvow.install import _body_digest, install_repo, place_policy, stamp_default
|
|
6
|
+
from gitvow.policy import DEFAULT_POLICY_PATH
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _default_text():
|
|
10
|
+
with open(DEFAULT_POLICY_PATH) as fh:
|
|
11
|
+
return fh.read()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_fresh_copy_is_stamped_and_a_rerun_changes_nothing(repo):
|
|
15
|
+
p = repo / ".gitvow" / "policy.json"
|
|
16
|
+
msg = place_policy(str(p), str(repo))
|
|
17
|
+
assert msg.startswith("default policy")
|
|
18
|
+
data = json.loads(p.read_text())
|
|
19
|
+
assert data["_from_default"] == _body_digest(data)
|
|
20
|
+
before = p.read_text()
|
|
21
|
+
assert place_policy(str(p), str(repo)) is None and p.read_text() == before
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_untouched_copy_of_an_older_default_is_refreshed(repo):
|
|
25
|
+
p = repo / ".gitvow" / "policy.json"
|
|
26
|
+
p.parent.mkdir()
|
|
27
|
+
old = json.loads(_default_text())
|
|
28
|
+
old["bash_confirm"] = old["bash_confirm"][1:] # an older default: one rule fewer
|
|
29
|
+
old.pop("_doc_commit_window", None)
|
|
30
|
+
p.write_text(stamp_default(json.dumps(old)))
|
|
31
|
+
msg = place_policy(str(p), str(repo))
|
|
32
|
+
assert msg and "refreshed" in msg
|
|
33
|
+
now = json.loads(p.read_text())
|
|
34
|
+
assert len(now["bash_confirm"]) == len(json.loads(_default_text())["bash_confirm"])
|
|
35
|
+
assert now["_from_default"] == _body_digest(now)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_edited_copy_and_legacy_copy_are_left_alone(repo):
|
|
39
|
+
p = repo / ".gitvow" / "policy.json"
|
|
40
|
+
p.parent.mkdir()
|
|
41
|
+
edited = json.loads(stamp_default(_default_text()))
|
|
42
|
+
edited["bash_deny"].append({"pattern": "\\bmy-tool\\s+wipe\\b", "reason": "our own rule"})
|
|
43
|
+
p.write_text(json.dumps(edited, indent=2) + "\n")
|
|
44
|
+
msg = place_policy(str(p), str(repo))
|
|
45
|
+
assert msg and "edited after it was copied" in msg
|
|
46
|
+
assert any(r.get("reason") == "our own rule" for r in json.loads(p.read_text())["bash_deny"])
|
|
47
|
+
legacy = json.loads(_default_text()) # a copy from before the marker existed
|
|
48
|
+
p.write_text(json.dumps(legacy, indent=2) + "\n")
|
|
49
|
+
msg = place_policy(str(p), str(repo))
|
|
50
|
+
assert msg and "predates the marker" in msg
|
|
51
|
+
assert "_from_default" not in json.loads(p.read_text())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_install_repo_reports_the_policy_outcome(repo, home):
|
|
55
|
+
lines = install_repo(str(repo))
|
|
56
|
+
assert any(ln.startswith("default policy") for ln in lines)
|
|
57
|
+
lines = install_repo(str(repo))
|
|
58
|
+
assert any(ln.startswith("policy → ") for ln in lines)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|