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.
Files changed (78) hide show
  1. {gitvow-0.28.2/src/gitvow.egg-info → gitvow-0.28.4}/PKG-INFO +1 -1
  2. {gitvow-0.28.2 → gitvow-0.28.4}/pyproject.toml +1 -1
  3. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/default_policy.json +24 -4
  4. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/hooks/__init__.py +14 -9
  5. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/install.py +60 -10
  6. {gitvow-0.28.2 → gitvow-0.28.4/src/gitvow.egg-info}/PKG-INFO +1 -1
  7. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow.egg-info/SOURCES.txt +2 -0
  8. gitvow-0.28.4/tests/test_answered_confirm_commit.py +35 -0
  9. gitvow-0.28.4/tests/test_policy_refresh.py +58 -0
  10. {gitvow-0.28.2 → gitvow-0.28.4}/LICENSE +0 -0
  11. {gitvow-0.28.2 → gitvow-0.28.4}/README.md +0 -0
  12. {gitvow-0.28.2 → gitvow-0.28.4}/setup.cfg +0 -0
  13. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/__init__.py +0 -0
  14. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/__main__.py +0 -0
  15. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/adapters.py +0 -0
  16. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/claims.py +0 -0
  17. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/cli.py +0 -0
  18. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/collect.py +0 -0
  19. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/decisions.py +0 -0
  20. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/digest.py +0 -0
  21. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/export.py +0 -0
  22. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/pack.py +0 -0
  23. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/paths.py +0 -0
  24. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/policy.py +0 -0
  25. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/pricing.py +0 -0
  26. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/proposals.py +0 -0
  27. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/providers.py +0 -0
  28. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/recall.py +0 -0
  29. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/redact.py +0 -0
  30. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/report.py +0 -0
  31. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/rules.py +0 -0
  32. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/safewrite.py +0 -0
  33. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/scan.py +0 -0
  34. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/selftest.py +0 -0
  35. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/serve.py +0 -0
  36. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/snapshots.py +0 -0
  37. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/state.py +0 -0
  38. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/status.py +0 -0
  39. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/sync.py +0 -0
  40. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/targets.py +0 -0
  41. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow/transcript.py +0 -0
  42. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow.egg-info/dependency_links.txt +0 -0
  43. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow.egg-info/entry_points.txt +0 -0
  44. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow.egg-info/requires.txt +0 -0
  45. {gitvow-0.28.2 → gitvow-0.28.4}/src/gitvow.egg-info/top_level.txt +0 -0
  46. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_adapters.py +0 -0
  47. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_claims.py +0 -0
  48. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_collect.py +0 -0
  49. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_command_position.py +0 -0
  50. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_commit_window.py +0 -0
  51. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_cost.py +0 -0
  52. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_decisions.py +0 -0
  53. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_digest.py +0 -0
  54. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_e2e_globalopt.py +0 -0
  55. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_export.py +0 -0
  56. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_external_adapter.py +0 -0
  57. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_git_rules.py +0 -0
  58. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_hardening.py +0 -0
  59. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_hooks.py +0 -0
  60. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_install_cli.py +0 -0
  61. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_pack.py +0 -0
  62. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_packaging.py +0 -0
  63. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_policy.py +0 -0
  64. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_proposals.py +0 -0
  65. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_providers.py +0 -0
  66. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_recall.py +0 -0
  67. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_redact.py +0 -0
  68. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_report.py +0 -0
  69. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_rules.py +0 -0
  70. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_scan.py +0 -0
  71. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_serve.py +0 -0
  72. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_snapshots.py +0 -0
  73. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_status.py +0 -0
  74. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_sync.py +0 -0
  75. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_sync_http.py +0 -0
  76. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_targets.py +0 -0
  77. {gitvow-0.28.2 → gitvow-0.28.4}/tests/test_tiers.py +0 -0
  78. {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.2
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.2"
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
- if session_scope and prior and prior.get("answer") == "accepted" and prior.get("scope") == "session":
190
- log_event(cwd, "allowed_by_session_answer", {**payload, "by": prior.get("by")})
191
- return 0, ""
192
- if prior and prior.get("answer") == "declined":
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
- st = load_state(cwd)
196
- dec.add(cwd, [f], st.get("steps", 0) + 1, tool, rules)
197
- n = next((x["n"] for x in dec.open_findings(cwd) if x["finding"] == text), None)
198
- log_event(cwd, "confirm_required", {**payload, "finding": n})
199
- return 2, confirm_message(d, n, session_scope)
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
- if not os.path.exists(pol):
520
- from .policy import DEFAULT_POLICY_PATH
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
- if not os.path.exists(pol):
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.2
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