gitvow 0.34.0__tar.gz → 0.34.2__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 (94) hide show
  1. {gitvow-0.34.0/src/gitvow.egg-info → gitvow-0.34.2}/PKG-INFO +1 -1
  2. {gitvow-0.34.0 → gitvow-0.34.2}/pyproject.toml +1 -1
  3. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/hooks/__init__.py +25 -4
  4. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/install.py +12 -3
  5. {gitvow-0.34.0 → gitvow-0.34.2/src/gitvow.egg-info}/PKG-INFO +1 -1
  6. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_hooks_disabled.py +48 -3
  7. {gitvow-0.34.0 → gitvow-0.34.2}/LICENSE +0 -0
  8. {gitvow-0.34.0 → gitvow-0.34.2}/README.md +0 -0
  9. {gitvow-0.34.0 → gitvow-0.34.2}/setup.cfg +0 -0
  10. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/__init__.py +0 -0
  11. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/__main__.py +0 -0
  12. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/adapters.py +0 -0
  13. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/carry.py +0 -0
  14. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/claims.py +0 -0
  15. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/cli.py +0 -0
  16. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/collect.py +0 -0
  17. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/decisions.py +0 -0
  18. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/default_policy.json +0 -0
  19. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/digest.py +0 -0
  20. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/export.py +0 -0
  21. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/intent.py +0 -0
  22. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/notes.py +0 -0
  23. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/outcomes.py +0 -0
  24. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/pack.py +0 -0
  25. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/paths.py +0 -0
  26. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/policy.py +0 -0
  27. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/pricing.py +0 -0
  28. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/proposals.py +0 -0
  29. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/providers.py +0 -0
  30. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/reach.py +0 -0
  31. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/recall.py +0 -0
  32. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/redact.py +0 -0
  33. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/report.py +0 -0
  34. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/rules.py +0 -0
  35. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/safewrite.py +0 -0
  36. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/scan.py +0 -0
  37. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/selftest.py +0 -0
  38. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/serve.py +0 -0
  39. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/snapshots.py +0 -0
  40. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/state.py +0 -0
  41. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/status.py +0 -0
  42. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/sync.py +0 -0
  43. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/targets.py +0 -0
  44. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow/transcript.py +0 -0
  45. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow.egg-info/SOURCES.txt +0 -0
  46. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow.egg-info/dependency_links.txt +0 -0
  47. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow.egg-info/entry_points.txt +0 -0
  48. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow.egg-info/requires.txt +0 -0
  49. {gitvow-0.34.0 → gitvow-0.34.2}/src/gitvow.egg-info/top_level.txt +0 -0
  50. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_adapters.py +0 -0
  51. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_answered_confirm_commit.py +0 -0
  52. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_carry.py +0 -0
  53. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_claims.py +0 -0
  54. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_collect.py +0 -0
  55. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_command_position.py +0 -0
  56. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_commit_window.py +0 -0
  57. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_cost.py +0 -0
  58. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_decisions.py +0 -0
  59. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_defects_0_32_1.py +0 -0
  60. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_digest.py +0 -0
  61. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_duplicate_hooks.py +0 -0
  62. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_e2e_globalopt.py +0 -0
  63. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_export.py +0 -0
  64. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_external_adapter.py +0 -0
  65. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_git_rules.py +0 -0
  66. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_hardening.py +0 -0
  67. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_hooks.py +0 -0
  68. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_install_cli.py +0 -0
  69. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_intent.py +0 -0
  70. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_native_question.py +0 -0
  71. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_opencode.py +0 -0
  72. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_outcomes.py +0 -0
  73. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_pack.py +0 -0
  74. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_packaging.py +0 -0
  75. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_policy.py +0 -0
  76. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_policy_refresh.py +0 -0
  77. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_proposals.py +0 -0
  78. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_providers.py +0 -0
  79. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_reach.py +0 -0
  80. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_recall.py +0 -0
  81. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_redact.py +0 -0
  82. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_report.py +0 -0
  83. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_rules.py +0 -0
  84. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_scan.py +0 -0
  85. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_serve.py +0 -0
  86. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_snapshots.py +0 -0
  87. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_status.py +0 -0
  88. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_status_environment.py +0 -0
  89. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_symlinked_paths.py +0 -0
  90. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_sync.py +0 -0
  91. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_sync_http.py +0 -0
  92. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_targets.py +0 -0
  93. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_tiers.py +0 -0
  94. {gitvow-0.34.0 → gitvow-0.34.2}/tests/test_transcript_formats.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitvow
3
- Version: 0.34.0
3
+ Version: 0.34.2
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.34.0"
7
+ version = "0.34.2"
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"}
@@ -275,6 +275,13 @@ def pre_tool_use(h: dict[str, Any], home: str | None = None) -> tuple[int, str]:
275
275
  "observed" if d.observed else "finding",
276
276
  {"tool": tool, "reason": d.reason[:200], "new": new, "session_id": h.get("session_id")},
277
277
  )
278
+ if tool == "Bash" and PUSH_RE.search(inp.get("command", "")) and not os.environ.get("GITVOW_PUSHING_NOTES"):
279
+ # The agent is about to push. The pre-push hook steps aside while this is fresh, and PostToolUse pushes
280
+ # the notes after the branch has landed, so the two pushes never overlap on the server.
281
+ st = load_state(cwd)
282
+ st["pending_push"] = time.time()
283
+ st["pending_ttl"] = int((pol.get("decisions") or {}).get("commit_window_seconds", 1800))
284
+ save_state(cwd, st)
278
285
  if tool == "Bash" and COMMIT_RE.search(inp.get("command", "")):
279
286
  pending = dec.undecided(cwd)
280
287
  mode = (pol.get("decisions") or {}).get("mode", "open")
@@ -462,6 +469,21 @@ def post_tool_use(h: dict[str, Any], home: str | None = None) -> tuple[int, str]
462
469
  cwd = target_cwd(tool, inp, session_cwd)
463
470
  if _duplicate(h, cwd, "post"):
464
471
  return 0, ""
472
+ code, msg = _post_tool_use(h, home, tool, inp, cwd)
473
+ if tool == "Bash" and PUSH_RE.search(inp.get("command", "")) and not os.environ.get("GITVOW_PUSHING_NOTES"):
474
+ # The notes travel after the branch, never alongside it: the pre-push hook stepped aside for this push
475
+ # (or never ran, as under Gemini). Done last, after any note for a `commit && push` chain was attached,
476
+ # so the note travels with the push it belongs to.
477
+ st = load_state(cwd)
478
+ if "pending_push" in st:
479
+ st.pop("pending_push", None)
480
+ save_state(cwd, st)
481
+ _push_notes_after(cwd, inp.get("command", ""))
482
+ return code, msg
483
+
484
+
485
+ def _post_tool_use(h: dict[str, Any], home: str | None, tool: str, inp: dict[str, Any], cwd: str) -> tuple[int, str]:
486
+ session_cwd = h.get("cwd") or os.getcwd()
465
487
  if tool in EDIT_TOOLS:
466
488
  fp = str(inp.get("file_path") or inp.get("notebook_path") or "")
467
489
  if fp:
@@ -482,9 +504,6 @@ def post_tool_use(h: dict[str, Any], home: str | None = None) -> tuple[int, str]
482
504
  return 0, ""
483
505
  if tool == "Bash" and not COMMIT_RE.search(inp.get("command", "")):
484
506
  _record_prompt_approval(cwd, inp, home)
485
- if hooks_disabled_by_env() and PUSH_RE.search(inp.get("command", "")):
486
- # the pre-push hook that carries the notes never ran; carry them now, after the push it belongs to
487
- _push_notes_after(cwd, inp.get("command", ""))
488
507
  return 0, ""
489
508
  if tool != "Bash":
490
509
  return 0, ""
@@ -662,7 +681,9 @@ def _push_notes_after(cwd: str, command: str) -> None:
662
681
  capture_output=True,
663
682
  text=True,
664
683
  )
665
- log_event(cwd, "notes_pushed_by_hook", {"remote": remote, "ok": r.returncode == 0, "error": (r.stderr or "")[:120]})
684
+ log_event(
685
+ cwd, "notes_pushed_after_push", {"remote": remote, "ok": r.returncode == 0, "error": (r.stderr or "")[:120]}
686
+ )
666
687
 
667
688
 
668
689
  def stop(h: dict[str, Any], home: str | None = None) -> tuple[int, str]:
@@ -72,11 +72,20 @@ PRE_PUSH_HOOK = """#!/bin/sh
72
72
  # gitvow: push session notes (refs/notes/gitvow/*) to the same remote whenever a branch is pushed.
73
73
  # The inner push re-enters this hook; GITVOW_PUSHING_NOTES stops the recursion.
74
74
  REMOTE="$1"
75
- if [ -z "$GITVOW_PUSHING_NOTES" ] && [ -n "$REMOTE" ] && git for-each-ref --count=1 refs/notes/gitvow/ | grep -q .; then
75
+ GD0="$(git rev-parse --git-dir)"; STATE0="$GD0/gitvow-session.json"
76
+ # An agent's push: the gate saw the command and PostToolUse pushes the notes *after* the branch lands, so this
77
+ # hook stays out of the way. Two pushes overlapping on the server made GitHub reject the branch with
78
+ # "fatal error in commit_refs" (seen four times in a day of releases); one after the other never does.
79
+ AGENT_PUSH=""
80
+ if [ -f "$STATE0" ]; then
81
+ AGENT_PUSH=$(python3 -c "import json,time;s=json.load(open('$STATE0'));p=s.get('pending_push') or 0;w=s.get('pending_ttl') or 300;print('1' if time.time()-p<w else '')" 2>/dev/null)
82
+ fi
83
+ if [ -z "$GITVOW_PUSHING_NOTES" ] && [ -z "$AGENT_PUSH" ] && [ -n "$REMOTE" ] && git for-each-ref --count=1 refs/notes/gitvow/ | grep -q .; then
76
84
  # Errors used to be discarded, so a notes push that failed left a record nobody could see was missing.
77
85
  # The failure is now said once, on the hook's stderr, and never stops the branch push.
78
- GD0="$(git rev-parse --git-dir)"
79
- if ! GITVOW_PUSHING_NOTES=1 git push --quiet "$REMOTE" 'refs/notes/gitvow/*:refs/notes/gitvow/*' 2>"$GD0/gitvow-notes-push.err" </dev/null; then
86
+ if GITVOW_PUSHING_NOTES=1 git push --quiet "$REMOTE" 'refs/notes/gitvow/*:refs/notes/gitvow/*' 2>"$GD0/gitvow-notes-push.err" </dev/null; then
87
+ sleep 1 # let the server finish committing the notes refs before the branch push reaches it
88
+ else
80
89
  echo "gitvow: pushing refs/notes/gitvow/* to $REMOTE failed ($(head -c 200 "$GD0/gitvow-notes-push.err" | tr '\\n' ' ')); run: gitvow push-notes $REMOTE" >&2
81
90
  fi
82
91
  fi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitvow
3
- Version: 0.34.0
3
+ Version: 0.34.2
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
@@ -113,14 +113,15 @@ def test_notes_are_pushed_after_the_agents_push_when_hooks_are_off(repo, home, p
113
113
  check=True,
114
114
  )
115
115
  assert git(bare, "for-each-ref", "refs/notes/") == ""
116
- # hooks on: gitvow trusts the pre-push hook and does nothing here
116
+ # after any agent push the notes follow, sequentially (0.34.1): hooks on or off makes no difference here
117
117
  post_tool_use(payload("PostToolUse", "Bash", {"command": "git push origin main"}), str(home))
118
- assert git(bare, "for-each-ref", "refs/notes/") == ""
118
+ assert git(bare, "rev-parse", "refs/notes/gitvow/s1") == git(repo, "rev-parse", "refs/notes/gitvow/s1")
119
+ assert '"kind": "notes_pushed_after_push"' in (repo / ".git" / "gitvow-hooks.log").read_text()
119
120
  for k, v in GEMINI_ENV.items():
120
121
  monkeypatch.setenv(k, v)
122
+ git(repo, "notes", "--ref=gitvow/s1", "add", "-f", "-m", 'gitvow-session\n{"x": 1}', "HEAD")
121
123
  post_tool_use(payload("PostToolUse", "Bash", {"command": "git push origin main"}), str(home))
122
124
  assert git(bare, "rev-parse", "refs/notes/gitvow/s1") == git(repo, "rev-parse", "refs/notes/gitvow/s1")
123
- assert '"kind": "notes_pushed_by_hook"' in (repo / ".git" / "gitvow-hooks.log").read_text()
124
125
 
125
126
 
126
127
  def test_identity_falls_back_to_the_gitconfig_file_gemini_hides(repo, home, monkeypatch):
@@ -132,3 +133,47 @@ def test_identity_falls_back_to_the_gitconfig_file_gemini_hides(repo, home, monk
132
133
  monkeypatch.delenv("GIT_CONFIG_GLOBAL")
133
134
  monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(home / ".gitconfig"))
134
135
  assert dec.identity(str(repo))[0] == "priya" # the ordinary path still reads it
136
+
137
+
138
+ def test_the_pre_push_hook_steps_aside_for_an_agents_push(repo, home, payload, tmp_path):
139
+ """The gate marks the agent's push; the hook then leaves the notes to PostToolUse, so the two never overlap."""
140
+ bare = tmp_path / "remote.git"
141
+ subprocess.run(["git", "init", "-q", "--bare", str(bare)], check=True)
142
+ git(repo, "remote", "add", "origin", str(bare))
143
+ _write_git_hook(str(repo / ".gitvow" / "git-hooks"))
144
+ git(repo, "config", "core.hooksPath", ".gitvow/git-hooks")
145
+ git(repo, "notes", "--ref=gitvow/s1", "add", "-m", "gitvow-session\n{}", "HEAD")
146
+ session_start(payload("SessionStart"), str(home))
147
+ code, _ = pre_tool_use(payload("PreToolUse", "Bash", {"command": "git push origin feature"}), str(home))
148
+ assert code == 0
149
+ st = json.loads((repo / ".git" / "gitvow-session.json").read_text())
150
+ assert st["pending_push"] and st["pending_ttl"] == 1800
151
+ subprocess.run(["git", "push", "-q", "origin", "HEAD:refs/heads/feature"], cwd=repo, check=True)
152
+ assert git(bare, "for-each-ref", "refs/notes/") == "" # the hook stepped aside
153
+ post_tool_use(payload("PostToolUse", "Bash", {"command": "git push origin feature"}), str(home))
154
+ assert git(bare, "rev-parse", "refs/notes/gitvow/s1") == git(repo, "rev-parse", "refs/notes/gitvow/s1")
155
+ assert "pending_push" not in json.loads((repo / ".git" / "gitvow-session.json").read_text())
156
+ # a person's push, no gate involved: the hook carries the notes itself
157
+ git(repo, "notes", "--ref=gitvow/s1", "add", "-f", "-m", 'gitvow-session\n{"y": 2}', "HEAD")
158
+ subprocess.run(["git", "push", "-q", "origin", "HEAD:refs/heads/feature2"], cwd=repo, check=True)
159
+ assert git(bare, "rev-parse", "refs/notes/gitvow/s1") == git(repo, "rev-parse", "refs/notes/gitvow/s1")
160
+
161
+
162
+ def test_a_commit_and_push_chain_sends_the_note_with_the_push(repo, home, payload, transcript, tmp_path):
163
+ """`git commit … && git push` takes the commit path; the notes must still follow, after the note is attached."""
164
+ bare = tmp_path / "remote.git"
165
+ subprocess.run(["git", "init", "-q", "--bare", str(bare)], check=True)
166
+ git(repo, "remote", "add", "origin", str(bare))
167
+ _write_git_hook(str(repo / ".gitvow" / "git-hooks"))
168
+ git(repo, "config", "core.hooksPath", ".gitvow/git-hooks")
169
+ session_start(payload("SessionStart"), str(home))
170
+ cmd = "git commit -am x && git push origin HEAD:refs/heads/feature" # not protected: no question
171
+ assert pre_tool_use(payload("PreToolUse", "Bash", {"command": cmd}, transcript), str(home))[0] == 0
172
+ (repo / "a.txt").write_text("chain\n")
173
+ git(repo, "commit", "-qam", "chain")
174
+ subprocess.run(["git", "push", "-q", "origin", "HEAD:refs/heads/feature"], cwd=repo, check=True)
175
+ assert git(bare, "for-each-ref", "refs/notes/") == "" # the hook stepped aside for the agent's push
176
+ code, msg = post_tool_use(payload("PostToolUse", "Bash", {"command": cmd}, transcript), str(home))
177
+ assert code == 0 and "session note attached" in msg
178
+ local = git(repo, "rev-parse", "refs/notes/gitvow/sess-1")
179
+ assert git(bare, "rev-parse", "refs/notes/gitvow/sess-1") == local # the note just written is on the remote
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