pygittools 0.2.1__tar.gz → 0.2.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 (84) hide show
  1. {pygittools-0.2.1 → pygittools-0.2.2}/PKG-INFO +1 -2
  2. {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/README.md +18 -0
  3. pygittools-0.2.2/hook_samples/pre-receive.branch-permissions +69 -0
  4. pygittools-0.2.2/mcp_server.py +8 -0
  5. {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/PKG-INFO +1 -2
  6. {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/SOURCES.txt +2 -0
  7. {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/entry_points.txt +0 -1
  8. {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/requires.txt +0 -1
  9. {pygittools-0.2.1 → pygittools-0.2.2}/pyproject.toml +1 -3
  10. {pygittools-0.2.1 → pygittools-0.2.2}/tasks.py +4 -0
  11. {pygittools-0.2.1 → pygittools-0.2.2}/tui/worktree.py +6 -4
  12. {pygittools-0.2.1 → pygittools-0.2.2}/tui/worktree_test.py +23 -0
  13. pygittools-0.2.1/mcp_server.py +0 -120
  14. {pygittools-0.2.1 → pygittools-0.2.2}/LICENSE +0 -0
  15. {pygittools-0.2.1 → pygittools-0.2.2}/README.md +0 -0
  16. {pygittools-0.2.1 → pygittools-0.2.2}/__init__.py +0 -0
  17. {pygittools-0.2.1 → pygittools-0.2.2}/__meta__.py +0 -0
  18. {pygittools-0.2.1 → pygittools-0.2.2}/help.md +0 -0
  19. {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/commit-msg.pattern +0 -0
  20. {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/post-commit.notify +0 -0
  21. {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/post-commit.push-remotes +0 -0
  22. {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/post-receive.notify +0 -0
  23. {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/pre-commit.ruff +0 -0
  24. {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/pre-receive.protected-pattern +0 -0
  25. {pygittools-0.2.1 → pygittools-0.2.2}/hooks.py +0 -0
  26. {pygittools-0.2.1 → pygittools-0.2.2}/hooks_notify.py +0 -0
  27. {pygittools-0.2.1 → pygittools-0.2.2}/hooks_patterns.py +0 -0
  28. {pygittools-0.2.1 → pygittools-0.2.2}/hooks_patterns_test.py +0 -0
  29. {pygittools-0.2.1 → pygittools-0.2.2}/hooks_push.py +0 -0
  30. {pygittools-0.2.1 → pygittools-0.2.2}/hooks_push_test.py +0 -0
  31. {pygittools-0.2.1 → pygittools-0.2.2}/hooks_ruff.py +0 -0
  32. {pygittools-0.2.1 → pygittools-0.2.2}/main.py +0 -0
  33. {pygittools-0.2.1 → pygittools-0.2.2}/merge.py +0 -0
  34. {pygittools-0.2.1 → pygittools-0.2.2}/merge_test.py +0 -0
  35. {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/dependency_links.txt +0 -0
  36. {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/top_level.txt +0 -0
  37. {pygittools-0.2.1 → pygittools-0.2.2}/setup.cfg +0 -0
  38. {pygittools-0.2.1 → pygittools-0.2.2}/tags_sync.py +0 -0
  39. {pygittools-0.2.1 → pygittools-0.2.2}/tags_sync_test.py +0 -0
  40. {pygittools-0.2.1 → pygittools-0.2.2}/tasks_query.py +0 -0
  41. {pygittools-0.2.1 → pygittools-0.2.2}/tasks_query_test.py +0 -0
  42. {pygittools-0.2.1 → pygittools-0.2.2}/tasks_test.py +0 -0
  43. {pygittools-0.2.1 → pygittools-0.2.2}/tui/__init__.py +0 -0
  44. {pygittools-0.2.1 → pygittools-0.2.2}/tui/app.py +0 -0
  45. {pygittools-0.2.1 → pygittools-0.2.2}/tui/app_test.py +0 -0
  46. {pygittools-0.2.1 → pygittools-0.2.2}/tui/branches.py +0 -0
  47. {pygittools-0.2.1 → pygittools-0.2.2}/tui/branches_test.py +0 -0
  48. {pygittools-0.2.1 → pygittools-0.2.2}/tui/changes_list.py +0 -0
  49. {pygittools-0.2.1 → pygittools-0.2.2}/tui/changes_list_test.py +0 -0
  50. {pygittools-0.2.1 → pygittools-0.2.2}/tui/colors.json +0 -0
  51. {pygittools-0.2.1 → pygittools-0.2.2}/tui/commit_draft.py +0 -0
  52. {pygittools-0.2.1 → pygittools-0.2.2}/tui/commit_draft_test.py +0 -0
  53. {pygittools-0.2.1 → pygittools-0.2.2}/tui/conftest.py +0 -0
  54. {pygittools-0.2.1 → pygittools-0.2.2}/tui/coverage_gaps_test.py +0 -0
  55. {pygittools-0.2.1 → pygittools-0.2.2}/tui/draw.py +0 -0
  56. {pygittools-0.2.1 → pygittools-0.2.2}/tui/draw_test.py +0 -0
  57. {pygittools-0.2.1 → pygittools-0.2.2}/tui/git_colors.py +0 -0
  58. {pygittools-0.2.1 → pygittools-0.2.2}/tui/git_colors_test.py +0 -0
  59. {pygittools-0.2.1 → pygittools-0.2.2}/tui/input_context.py +0 -0
  60. {pygittools-0.2.1 → pygittools-0.2.2}/tui/input_context_test.py +0 -0
  61. {pygittools-0.2.1 → pygittools-0.2.2}/tui/log_list.py +0 -0
  62. {pygittools-0.2.1 → pygittools-0.2.2}/tui/log_list_test.py +0 -0
  63. {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/__init__.py +0 -0
  64. {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/branches.py +0 -0
  65. {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/help.py +0 -0
  66. {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/home.py +0 -0
  67. {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/no_repo.py +0 -0
  68. {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/projects.py +0 -0
  69. {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/task_editor.py +0 -0
  70. {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages_test.py +0 -0
  71. {pygittools-0.2.1 → pygittools-0.2.2}/tui/project_label.py +0 -0
  72. {pygittools-0.2.1 → pygittools-0.2.2}/tui/projects.py +0 -0
  73. {pygittools-0.2.1 → pygittools-0.2.2}/tui/projects_test.py +0 -0
  74. {pygittools-0.2.1 → pygittools-0.2.2}/tui/pygitweb_layout.py +0 -0
  75. {pygittools-0.2.1 → pygittools-0.2.2}/tui/pygitweb_layout_test.py +0 -0
  76. {pygittools-0.2.1 → pygittools-0.2.2}/tui/repo.py +0 -0
  77. {pygittools-0.2.1 → pygittools-0.2.2}/tui/repo_test.py +0 -0
  78. {pygittools-0.2.1 → pygittools-0.2.2}/tui/status_bar.py +0 -0
  79. {pygittools-0.2.1 → pygittools-0.2.2}/tui/status_bar_test.py +0 -0
  80. {pygittools-0.2.1 → pygittools-0.2.2}/tui/tabs.py +0 -0
  81. {pygittools-0.2.1 → pygittools-0.2.2}/tui/tabs_test.py +0 -0
  82. {pygittools-0.2.1 → pygittools-0.2.2}/tui/tasks_list.py +0 -0
  83. {pygittools-0.2.1 → pygittools-0.2.2}/tui/types.py +0 -0
  84. {pygittools-0.2.1 → pygittools-0.2.2}/tui/ucurses.py +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygittools
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Hooks and task storage in Git
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
- Requires-Dist: mcp>=1.28.0
10
9
  Requires-Dist: pygit2>=1.12.0
11
10
  Requires-Dist: uni-curses>=3.1.2
12
11
  Dynamic: license-file
@@ -30,6 +30,24 @@ Environment variables:
30
30
  - `PYGITWEB_COMMIT_MSG_PATTERN` — message regex (default `^(\S+): (.+)`)
31
31
  - `PYGITWEB_REJECT_MERGE_COMMITS` — set to `1`/`true`/`yes` to reject merge commits
32
32
 
33
+ ## pre-receive.branch-permissions
34
+
35
+ Enforce per-branch write permissions based on `auth.json` `oauth_permissions`. For each
36
+ updated branch ref (`refs/heads/*`), the hook:
37
+
38
+ - Builds a `project/branch` subject (e.g. `demo/main`, `demo/dev/alice.feature`).
39
+ - If any `pgw.write_branch.{scope}` keys match that subject, requires a matching grant.
40
+ - Otherwise, falls back to project write (`pgw.write.{project}`) for that project.
41
+
42
+ When no usable configuration or context is available, the hook is a no-op and allows
43
+ the push to proceed.
44
+
45
+ Environment variables:
46
+
47
+ - `PYGITWEB_PRINCIPAL` — principal identity string (email or username) to check grants for.
48
+ - `PYGITWEB_PROJECT` — project path as known to pygitweb (e.g. `demo`, `team/app`).
49
+ - `PYGITWEB_AUTH_CONFIG` — optional auth.json path (default: `~/.pygitweb/auth.json`).
50
+
33
51
  ## post-commit.push-remotes
34
52
 
35
53
  After each local commit, force-with-lease push the current branch to every configured
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env -S uv run python
2
+
3
+ from __future__ import annotations
4
+
5
+ __VERSION__ = "1"
6
+
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from pygittools.hooks import HookResult, RefUpdate, parse_ref_updates
12
+ from pygitweb.auth_config import DEFAULT_AUTH_CONFIG_PATH, AuthConfig
13
+ from pygitweb.permissions import PermissionsMap, has_branch_write_permission
14
+
15
+
16
+ def _normalize_env(name: str) -> str | None:
17
+ value = os.environ.get(name)
18
+ if value is None:
19
+ return None
20
+ stripped = value.strip()
21
+ return stripped or None
22
+
23
+
24
+ def _load_permissions() -> PermissionsMap | None:
25
+ config_path_env = _normalize_env("PYGITWEB_AUTH_CONFIG")
26
+ path = Path(config_path_env).expanduser() if config_path_env is not None else DEFAULT_AUTH_CONFIG_PATH
27
+ try:
28
+ text = path.read_text(encoding="utf-8")
29
+ except OSError:
30
+ return None
31
+ try:
32
+ config = AuthConfig.model_validate_json(text)
33
+ except Exception:
34
+ return None
35
+ return config.oauth_permissions
36
+
37
+
38
+ def _check_updates(
39
+ principal: str,
40
+ project: str,
41
+ updates: list[RefUpdate],
42
+ permissions: PermissionsMap,
43
+ ) -> HookResult:
44
+ for _, _, ref_name in updates:
45
+ if has_branch_write_permission(permissions, principal, project, ref_name):
46
+ continue
47
+ sys.stderr.write(
48
+ f"Push rejected: {principal!r} lacks branch write permission for project {project!r} ref {ref_name!r}.\n",
49
+ )
50
+ return HookResult.FAILURE
51
+ return HookResult.SUCCESS
52
+
53
+
54
+ def main() -> int:
55
+ principal = _normalize_env("PYGITWEB_PRINCIPAL")
56
+ project = _normalize_env("PYGITWEB_PROJECT")
57
+ if principal is None or project is None:
58
+ return int(HookResult.SUCCESS.value)
59
+ permissions = _load_permissions()
60
+ if permissions is None:
61
+ return int(HookResult.SUCCESS.value)
62
+ updates = parse_ref_updates(sys.stdin)
63
+ result = _check_updates(principal, project, updates, permissions)
64
+ return int(result.value)
65
+
66
+
67
+ if __name__ == "__main__":
68
+ raise SystemExit(main())
69
+
@@ -0,0 +1,8 @@
1
+ """Compatibility shim for the MCP server.
2
+
3
+ Use the dedicated ``pygittools-mcp`` package for the actual implementation.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pygittools_mcp.mcp_server import * # noqa: F401,F403
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygittools
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Hooks and task storage in Git
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
- Requires-Dist: mcp>=1.28.0
10
9
  Requires-Dist: pygit2>=1.12.0
11
10
  Requires-Dist: uni-curses>=3.1.2
12
11
  Dynamic: license-file
@@ -47,6 +47,7 @@ tasks_test.py
47
47
  ./hook_samples/post-commit.push-remotes
48
48
  ./hook_samples/post-receive.notify
49
49
  ./hook_samples/pre-commit.ruff
50
+ ./hook_samples/pre-receive.branch-permissions
50
51
  ./hook_samples/pre-receive.protected-pattern
51
52
  ./tui/__init__.py
52
53
  ./tui/app.py
@@ -98,6 +99,7 @@ hook_samples/post-commit.notify
98
99
  hook_samples/post-commit.push-remotes
99
100
  hook_samples/post-receive.notify
100
101
  hook_samples/pre-commit.ruff
102
+ hook_samples/pre-receive.branch-permissions
101
103
  hook_samples/pre-receive.protected-pattern
102
104
  pygittools.egg-info/PKG-INFO
103
105
  pygittools.egg-info/SOURCES.txt
@@ -1,3 +1,2 @@
1
1
  [console_scripts]
2
2
  pgt = pygittools.main:main
3
- pygittools-mcp = pygittools.mcp_server:main
@@ -1,3 +1,2 @@
1
- mcp>=1.28.0
2
1
  pygit2>=1.12.0
3
2
  uni-curses>=3.1.2
@@ -4,21 +4,19 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pygittools"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Hooks and task storage in Git"
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  license-files = ["LICENSE"]
12
12
  requires-python = ">=3.11"
13
13
  dependencies = [
14
- "mcp>=1.28.0",
15
14
  "pygit2>=1.12.0",
16
15
  "uni-curses>=3.1.2",
17
16
  ]
18
17
 
19
18
  [project.scripts]
20
19
  pgt = "pygittools.main:main"
21
- pygittools-mcp = "pygittools.mcp_server:main"
22
20
 
23
21
  [tool.setuptools]
24
22
  packages = ["pygittools", "pygittools.tui", "pygittools.tui.pages"]
@@ -108,6 +108,7 @@ class Task:
108
108
  priority: Priority | None = None,
109
109
  assignee: str | None = None,
110
110
  due_date: datetime | None = None,
111
+ tags: str = "",
111
112
  ):
112
113
  if name is None:
113
114
  name = f"{TASK_REF_PREFIX}{title.lower().replace(' ', '_')}"
@@ -124,6 +125,7 @@ class Task:
124
125
  self.priority = priority
125
126
  self.assignee = assignee
126
127
  self.due_date = due_date
128
+ self.tags = tags
127
129
  self.created_at = datetime.now()
128
130
  self.comments: list[Oid | str] = [] # Comment OIDs
129
131
  self.update_message()
@@ -180,6 +182,7 @@ def get_task(repo: Repository, ref: str) -> Task | None:
180
182
  priority=priority,
181
183
  assignee=j.get("assignee"),
182
184
  due_date=_parse_dt(j.get("due_date")),
185
+ tags=j.get("tags", "") or "",
183
186
  )
184
187
  t.comments = j.get("comments", [])
185
188
  t.created_at = _parse_dt(j.get("created_at")) or t.created_at
@@ -205,6 +208,7 @@ def get_task_by_oid(repo: Repository, oid: Oid) -> Task | None:
205
208
  priority=priority,
206
209
  assignee=j.get("assignee"),
207
210
  due_date=_parse_dt(j.get("due_date")),
211
+ tags=j.get("tags", "") or "",
208
212
  )
209
213
  t.comments = j.get("comments", [])
210
214
  t.created_at = _parse_dt(j.get("created_at")) or t.created_at
@@ -44,9 +44,7 @@ def _index_path(path: str) -> str:
44
44
 
45
45
 
46
46
  def stage_path(repo: Repository, path: str) -> None:
47
- index = repo.index
48
- index.add(_index_path(path))
49
- index.write()
47
+ stage_paths(repo, [path])
50
48
 
51
49
 
52
50
  def stage_paths(repo: Repository, paths: list[str]) -> None:
@@ -54,7 +52,11 @@ def stage_paths(repo: Repository, paths: list[str]) -> None:
54
52
  return
55
53
  index = repo.index
56
54
  for path in paths:
57
- index.add(_index_path(path))
55
+ status_flags = repo.status().get(path, 0)
56
+ if status_flags & FileStatus.WT_DELETED:
57
+ index.remove(path)
58
+ else:
59
+ index.add(_index_path(path))
58
60
  index.write()
59
61
 
60
62
 
@@ -77,6 +77,29 @@ def test_stage_paths_accepts_untracked_directory_trailing_slash(repo: Repository
77
77
  assert status["loose.txt"] & FileStatus.INDEX_NEW
78
78
 
79
79
 
80
+ def test_stage_paths_stages_deleted_files(committed_repo: Repository) -> None:
81
+ workdir = Path(committed_repo.workdir)
82
+ (workdir / "tracked.txt").unlink()
83
+
84
+ _, unstaged, _ = categorize_status(committed_repo)
85
+ assert "tracked.txt" in unstaged
86
+
87
+ stage_paths(committed_repo, unstaged)
88
+
89
+ status = committed_repo.status()
90
+ assert status["tracked.txt"] & FileStatus.INDEX_DELETED
91
+
92
+
93
+ def test_stage_path_stages_deleted_file(committed_repo: Repository) -> None:
94
+ workdir = Path(committed_repo.workdir)
95
+ (workdir / "tracked.txt").unlink()
96
+
97
+ stage_path(committed_repo, "tracked.txt")
98
+
99
+ status = committed_repo.status()
100
+ assert status["tracked.txt"] & FileStatus.INDEX_DELETED
101
+
102
+
80
103
  def test_stage_paths_noop_for_empty_list(repo: Repository) -> None:
81
104
  stage_paths(repo, [])
82
105
 
@@ -1,120 +0,0 @@
1
- """MCP server exposing pygittools task boards to AI clients."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
-
8
- from mcp.server.fastmcp import FastMCP
9
-
10
- from pygittools.tasks_query import (
11
- BoardColumnView,
12
- BoardView,
13
- RepoLocation,
14
- TaskView,
15
- discover_repo_path,
16
- get_board_tasks_grouped,
17
- get_task_view,
18
- list_boards,
19
- list_tasks,
20
- open_repository,
21
- resolve_repo_location,
22
- )
23
-
24
- mcp = FastMCP(
25
- "pygittools-tasks",
26
- instructions=(
27
- "Read task boards stored as Git annotated tags. "
28
- "Boards live under refs/tags/boards/, tasks under refs/tags/tasks/, "
29
- "and comments under refs/tags/comments/. "
30
- "Set PYGITTOOLS_REPO to a repository path, or PYGITWEB_PROJECTROOT plus a project name."
31
- ),
32
- json_response=True,
33
- )
34
-
35
-
36
- def _repo_for_tool(project: str | None = None) -> RepoLocation:
37
- return resolve_repo_location(project=project)
38
-
39
-
40
- @mcp.tool(title="List boards")
41
- def list_boards_tool(project: str | None = None) -> list[BoardView]:
42
- """List all task boards in the configured repository."""
43
- location = _repo_for_tool(project)
44
- repo = open_repository(location)
45
- return list_boards(repo)
46
-
47
-
48
- @mcp.tool(title="List tasks")
49
- def list_tasks_tool(
50
- board: str = "Tasks",
51
- project: str | None = None,
52
- include_comments: bool = True,
53
- ) -> list[TaskView]:
54
- """List tasks on a board, optionally including comment bodies."""
55
- location = _repo_for_tool(project)
56
- repo = open_repository(location)
57
- try:
58
- return list_tasks(repo, board, include_comments=include_comments)
59
- except KeyError as exc:
60
- raise ValueError(str(exc)) from exc
61
-
62
-
63
- @mcp.tool(title="Get task")
64
- def get_task_tool(task_ref: str, project: str | None = None) -> TaskView:
65
- """Get one task by ref slug (task_123) or full ref (refs/tags/tasks/task_123)."""
66
- location = _repo_for_tool(project)
67
- repo = open_repository(location)
68
- try:
69
- return get_task_view(repo, task_ref, include_comments=True)
70
- except KeyError as exc:
71
- raise ValueError(str(exc)) from exc
72
-
73
-
74
- @mcp.tool(title="Board grouped by status")
75
- def board_by_status_tool(
76
- board: str = "Tasks",
77
- project: str | None = None,
78
- include_comments: bool = True,
79
- ) -> list[BoardColumnView]:
80
- """Return board tasks grouped into status columns (TODO, IN_PROGRESS, etc.)."""
81
- location = _repo_for_tool(project)
82
- repo = open_repository(location)
83
- try:
84
- return get_board_tasks_grouped(repo, board, include_comments=include_comments)
85
- except KeyError as exc:
86
- raise ValueError(str(exc)) from exc
87
-
88
-
89
- @mcp.resource("board://{board_name}")
90
- def board_resource(board_name: str) -> str:
91
- """JSON snapshot of a board grouped by status."""
92
- location = resolve_repo_location()
93
- repo = open_repository(location)
94
- columns = get_board_tasks_grouped(repo, board_name, include_comments=True)
95
- return json.dumps(columns, indent=2)
96
-
97
-
98
- @mcp.resource("task://{task_ref}")
99
- def task_resource(task_ref: str) -> str:
100
- """JSON snapshot of a single task with comments."""
101
- location = resolve_repo_location()
102
- repo = open_repository(location)
103
- task = get_task_view(repo, task_ref, include_comments=True)
104
- return json.dumps(task, indent=2)
105
-
106
-
107
- def main() -> None:
108
- repo_env = os.environ.get("PYGITTOOLS_REPO", "").strip()
109
- if not repo_env:
110
- try:
111
- discovered = discover_repo_path()
112
- except FileNotFoundError:
113
- discovered = None
114
- if discovered is not None:
115
- os.environ["PYGITTOOLS_REPO"] = str(discovered)
116
- mcp.run(transport="stdio")
117
-
118
-
119
- if __name__ == "__main__":
120
- main()
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