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.
- {pygittools-0.2.1 → pygittools-0.2.2}/PKG-INFO +1 -2
- {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/README.md +18 -0
- pygittools-0.2.2/hook_samples/pre-receive.branch-permissions +69 -0
- pygittools-0.2.2/mcp_server.py +8 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/PKG-INFO +1 -2
- {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/SOURCES.txt +2 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/entry_points.txt +0 -1
- {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/requires.txt +0 -1
- {pygittools-0.2.1 → pygittools-0.2.2}/pyproject.toml +1 -3
- {pygittools-0.2.1 → pygittools-0.2.2}/tasks.py +4 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/worktree.py +6 -4
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/worktree_test.py +23 -0
- pygittools-0.2.1/mcp_server.py +0 -120
- {pygittools-0.2.1 → pygittools-0.2.2}/LICENSE +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/README.md +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/__init__.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/__meta__.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/help.md +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/commit-msg.pattern +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/post-commit.notify +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/post-commit.push-remotes +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/post-receive.notify +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/pre-commit.ruff +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hook_samples/pre-receive.protected-pattern +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hooks.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hooks_notify.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hooks_patterns.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hooks_patterns_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hooks_push.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hooks_push_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/hooks_ruff.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/main.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/merge.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/merge_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/dependency_links.txt +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/pygittools.egg-info/top_level.txt +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/setup.cfg +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tags_sync.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tags_sync_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tasks_query.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tasks_query_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tasks_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/__init__.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/app.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/app_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/branches.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/branches_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/changes_list.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/changes_list_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/colors.json +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/commit_draft.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/commit_draft_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/conftest.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/coverage_gaps_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/draw.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/draw_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/git_colors.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/git_colors_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/input_context.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/input_context_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/log_list.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/log_list_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/__init__.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/branches.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/help.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/home.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/no_repo.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/projects.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages/task_editor.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/pages_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/project_label.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/projects.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/projects_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/pygitweb_layout.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/pygitweb_layout_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/repo.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/repo_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/status_bar.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/status_bar_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/tabs.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/tabs_test.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/tasks_list.py +0 -0
- {pygittools-0.2.1 → pygittools-0.2.2}/tui/types.py +0 -0
- {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.
|
|
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
|
+
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygittools
|
|
3
|
-
Version: 0.2.
|
|
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
|
|
@@ -4,21 +4,19 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pygittools"
|
|
7
|
-
version = "0.2.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
pygittools-0.2.1/mcp_server.py
DELETED
|
@@ -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
|
|
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
|