quickcall-integrations 0.1.3__tar.gz → 0.1.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.
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/.gitignore +1 -0
- quickcall_integrations-0.1.3/README.md → quickcall_integrations-0.1.4/PKG-INFO +28 -4
- quickcall_integrations-0.1.3/PKG-INFO → quickcall_integrations-0.1.4/README.md +17 -13
- quickcall_integrations-0.1.4/mcp_server/api_clients/__init__.py +6 -0
- quickcall_integrations-0.1.4/mcp_server/api_clients/github_client.py +440 -0
- quickcall_integrations-0.1.4/mcp_server/api_clients/slack_client.py +359 -0
- quickcall_integrations-0.1.4/mcp_server/auth/__init__.py +24 -0
- quickcall_integrations-0.1.4/mcp_server/auth/credentials.py +278 -0
- quickcall_integrations-0.1.4/mcp_server/auth/device_flow.py +253 -0
- quickcall_integrations-0.1.4/mcp_server/server.py +94 -0
- quickcall_integrations-0.1.4/mcp_server/tools/__init__.py +13 -0
- quickcall_integrations-0.1.4/mcp_server/tools/auth_tools.py +411 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/mcp_server/tools/git_tools.py +42 -18
- quickcall_integrations-0.1.4/mcp_server/tools/github_tools.py +338 -0
- quickcall_integrations-0.1.4/mcp_server/tools/slack_tools.py +203 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/pyproject.toml +10 -1
- quickcall_integrations-0.1.4/tests/test_auth_flow.py +376 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/tests/test_tools.py +24 -17
- quickcall_integrations-0.1.4/uv.lock +1937 -0
- quickcall_integrations-0.1.3/mcp_server/config.py +0 -10
- quickcall_integrations-0.1.3/mcp_server/server.py +0 -42
- quickcall_integrations-0.1.3/mcp_server/tools/__init__.py +0 -1
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/.claude-plugin/marketplace.json +0 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/.github/workflows/publish-pypi.yml +0 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/Dockerfile +0 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/assets/logo.png +0 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/mcp_server/__init__.py +0 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/mcp_server/tools/utility_tools.py +0 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/plugins/quickcall/.claude-plugin/plugin.json +0 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/plugins/quickcall/.mcp.json +0 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/plugins/quickcall/commands/updates.md +0 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/requirements.txt +0 -0
- {quickcall_integrations-0.1.3 → quickcall_integrations-0.1.4}/tests/README.md +0 -0
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quickcall-integrations
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: MCP server with developer integrations for Claude Code and Cursor
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: fastmcp>=2.13.0
|
|
7
|
+
Requires-Dist: httpx>=0.28.0
|
|
8
|
+
Requires-Dist: pydantic>=2.11.7
|
|
9
|
+
Requires-Dist: pygithub>=2.8.1
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
1
12
|
<p align="center">
|
|
2
13
|
<img src="assets/logo.png" alt="QuickCall" width="400">
|
|
3
14
|
</p>
|
|
@@ -8,6 +19,11 @@
|
|
|
8
19
|
<em>Ask about your work, get instant answers. No more context switching.</em>
|
|
9
20
|
</p>
|
|
10
21
|
|
|
22
|
+
<p align="center">
|
|
23
|
+
<a href="https://quickcall.dev"><img src="https://img.shields.io/badge/Web-quickcall.dev-000000?logo=googlechrome&logoColor=white" alt="Web"></a>
|
|
24
|
+
<a href="https://discord.gg/DtnMxuE35v"><img src="https://img.shields.io/badge/Discord-Join%20Us-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
11
27
|
<p align="center">
|
|
12
28
|
<a href="#claude-code">Claude Code</a> |
|
|
13
29
|
<a href="#cursor">Cursor</a> |
|
|
@@ -17,12 +33,12 @@
|
|
|
17
33
|
|
|
18
34
|
---
|
|
19
35
|
|
|
20
|
-
|
|
36
|
+
## Current integrations
|
|
37
|
+
|
|
21
38
|
- Git - commits, diffs, code changes
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- Slack
|
|
40
|
+
## Coming soon
|
|
41
|
+
|
|
26
42
|
- GitHub PRs & Issues
|
|
27
43
|
|
|
28
44
|
## Install
|
|
@@ -73,6 +89,8 @@ Add to your Cursor MCP config (`~/.cursor/mcp.json` for global, or `.cursor/mcp.
|
|
|
73
89
|
|
|
74
90
|
Then restart Cursor.
|
|
75
91
|
|
|
92
|
+
> Also works with [Antigravity](https://antigravity.dev) and any other IDE that supports MCP servers.
|
|
93
|
+
|
|
76
94
|
## Commands
|
|
77
95
|
|
|
78
96
|
### Claude Code
|
|
@@ -112,3 +130,9 @@ git push origin v0.1.0
|
|
|
112
130
|
```
|
|
113
131
|
|
|
114
132
|
Or trigger manually from GitHub Actions page.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
<p align="center">
|
|
137
|
+
Built with ❤️ by <a href="https://quickcall.dev">QuickCall</a>
|
|
138
|
+
</p>
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: quickcall-integrations
|
|
3
|
-
Version: 0.1.3
|
|
4
|
-
Summary: MCP server with developer integrations for Claude Code and Cursor
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: fastmcp>=2.13.0
|
|
7
|
-
Requires-Dist: pydantic>=2.11.7
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
1
|
<p align="center">
|
|
11
2
|
<img src="assets/logo.png" alt="QuickCall" width="400">
|
|
12
3
|
</p>
|
|
@@ -17,6 +8,11 @@ Description-Content-Type: text/markdown
|
|
|
17
8
|
<em>Ask about your work, get instant answers. No more context switching.</em>
|
|
18
9
|
</p>
|
|
19
10
|
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://quickcall.dev"><img src="https://img.shields.io/badge/Web-quickcall.dev-000000?logo=googlechrome&logoColor=white" alt="Web"></a>
|
|
13
|
+
<a href="https://discord.gg/DtnMxuE35v"><img src="https://img.shields.io/badge/Discord-Join%20Us-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
20
16
|
<p align="center">
|
|
21
17
|
<a href="#claude-code">Claude Code</a> |
|
|
22
18
|
<a href="#cursor">Cursor</a> |
|
|
@@ -26,12 +22,12 @@ Description-Content-Type: text/markdown
|
|
|
26
22
|
|
|
27
23
|
---
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
## Current integrations
|
|
26
|
+
|
|
30
27
|
- Git - commits, diffs, code changes
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- Slack
|
|
29
|
+
## Coming soon
|
|
30
|
+
|
|
35
31
|
- GitHub PRs & Issues
|
|
36
32
|
|
|
37
33
|
## Install
|
|
@@ -82,6 +78,8 @@ Add to your Cursor MCP config (`~/.cursor/mcp.json` for global, or `.cursor/mcp.
|
|
|
82
78
|
|
|
83
79
|
Then restart Cursor.
|
|
84
80
|
|
|
81
|
+
> Also works with [Antigravity](https://antigravity.dev) and any other IDE that supports MCP servers.
|
|
82
|
+
|
|
85
83
|
## Commands
|
|
86
84
|
|
|
87
85
|
### Claude Code
|
|
@@ -121,3 +119,9 @@ git push origin v0.1.0
|
|
|
121
119
|
```
|
|
122
120
|
|
|
123
121
|
Or trigger manually from GitHub Actions page.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
<p align="center">
|
|
126
|
+
Built with ❤️ by <a href="https://quickcall.dev">QuickCall</a>
|
|
127
|
+
</p>
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub API client for MCP server.
|
|
3
|
+
|
|
4
|
+
Provides GitHub API operations using PyGithub library.
|
|
5
|
+
Focuses on PRs and commits for minimal implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import List, Optional, Dict, Any
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from github import Github, GithubException, Auth
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ============================================================================
|
|
20
|
+
# Pydantic models for GitHub data
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Commit(BaseModel):
|
|
25
|
+
"""Represents a GitHub commit."""
|
|
26
|
+
|
|
27
|
+
sha: str
|
|
28
|
+
message: str
|
|
29
|
+
author: str
|
|
30
|
+
date: datetime
|
|
31
|
+
html_url: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PullRequest(BaseModel):
|
|
35
|
+
"""Represents a GitHub pull request."""
|
|
36
|
+
|
|
37
|
+
number: int
|
|
38
|
+
title: str
|
|
39
|
+
body: Optional[str] = None
|
|
40
|
+
state: str # open, closed
|
|
41
|
+
author: str
|
|
42
|
+
created_at: datetime
|
|
43
|
+
updated_at: Optional[datetime] = None
|
|
44
|
+
merged_at: Optional[datetime] = None
|
|
45
|
+
html_url: str
|
|
46
|
+
head_branch: str
|
|
47
|
+
base_branch: str
|
|
48
|
+
additions: int = 0
|
|
49
|
+
deletions: int = 0
|
|
50
|
+
changed_files: int = 0
|
|
51
|
+
commits: int = 0
|
|
52
|
+
draft: bool = False
|
|
53
|
+
mergeable: Optional[bool] = None
|
|
54
|
+
labels: List[str] = []
|
|
55
|
+
reviewers: List[str] = []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Repository(BaseModel):
|
|
59
|
+
"""Represents a GitHub repository."""
|
|
60
|
+
|
|
61
|
+
name: str
|
|
62
|
+
owner: str
|
|
63
|
+
full_name: str
|
|
64
|
+
html_url: str
|
|
65
|
+
description: str = ""
|
|
66
|
+
default_branch: str
|
|
67
|
+
private: bool = False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ============================================================================
|
|
71
|
+
# GitHub Client
|
|
72
|
+
# ============================================================================
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class GitHubClient:
|
|
76
|
+
"""
|
|
77
|
+
GitHub API client using PyGithub.
|
|
78
|
+
|
|
79
|
+
Provides simplified interface for GitHub operations.
|
|
80
|
+
Focuses on PRs and commits.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
token: str,
|
|
86
|
+
default_owner: Optional[str] = None,
|
|
87
|
+
default_repo: Optional[str] = None,
|
|
88
|
+
installation_id: Optional[int] = None,
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Initialize GitHub API client.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
token: GitHub installation access token
|
|
95
|
+
default_owner: Default repository owner (optional)
|
|
96
|
+
default_repo: Default repository name (optional)
|
|
97
|
+
installation_id: GitHub App installation ID (for listing repos)
|
|
98
|
+
"""
|
|
99
|
+
self.token = token
|
|
100
|
+
self.default_owner = default_owner
|
|
101
|
+
self.default_repo = default_repo
|
|
102
|
+
self.installation_id = installation_id
|
|
103
|
+
|
|
104
|
+
# Initialize PyGithub client
|
|
105
|
+
auth = Auth.Token(token)
|
|
106
|
+
self.gh = Github(auth=auth)
|
|
107
|
+
|
|
108
|
+
# Cache for repo objects
|
|
109
|
+
self._repo_cache: Dict[str, Any] = {}
|
|
110
|
+
|
|
111
|
+
def _get_repo(self, owner: Optional[str] = None, repo: Optional[str] = None):
|
|
112
|
+
"""Get PyGithub repo object, using defaults if not specified."""
|
|
113
|
+
owner = owner or self.default_owner
|
|
114
|
+
repo = repo or self.default_repo
|
|
115
|
+
|
|
116
|
+
if not owner or not repo:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
"Repository owner and name must be specified or set as defaults"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
full_name = f"{owner}/{repo}"
|
|
122
|
+
if full_name not in self._repo_cache:
|
|
123
|
+
self._repo_cache[full_name] = self.gh.get_repo(full_name)
|
|
124
|
+
|
|
125
|
+
return self._repo_cache[full_name]
|
|
126
|
+
|
|
127
|
+
def health_check(self) -> bool:
|
|
128
|
+
"""Check if GitHub API is accessible with the token."""
|
|
129
|
+
try:
|
|
130
|
+
self.gh.get_user().login
|
|
131
|
+
return True
|
|
132
|
+
except Exception:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
def get_authenticated_user(self) -> str:
|
|
136
|
+
"""Get the username of the authenticated user."""
|
|
137
|
+
return self.gh.get_user().login
|
|
138
|
+
|
|
139
|
+
def close(self):
|
|
140
|
+
"""Close GitHub API client."""
|
|
141
|
+
self.gh.close()
|
|
142
|
+
|
|
143
|
+
# ========================================================================
|
|
144
|
+
# Repository Operations
|
|
145
|
+
# ========================================================================
|
|
146
|
+
|
|
147
|
+
def list_repos(self, limit: int = 20) -> List[Repository]:
|
|
148
|
+
"""
|
|
149
|
+
List repositories accessible to the GitHub App installation.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
limit: Maximum repositories to return
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
List of repositories
|
|
156
|
+
"""
|
|
157
|
+
repos = []
|
|
158
|
+
try:
|
|
159
|
+
# Installation tokens can't use PyGithub's user.get_repos() endpoint
|
|
160
|
+
# Must use /installation/repositories endpoint directly (same as backend)
|
|
161
|
+
# https://docs.github.com/en/rest/apps/installations#list-repositories-accessible-to-the-app-installation
|
|
162
|
+
with httpx.Client() as client:
|
|
163
|
+
response = client.get(
|
|
164
|
+
"https://api.github.com/installation/repositories",
|
|
165
|
+
headers={
|
|
166
|
+
"Authorization": f"Bearer {self.token}",
|
|
167
|
+
"Accept": "application/vnd.github+json",
|
|
168
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
169
|
+
},
|
|
170
|
+
params={"per_page": limit},
|
|
171
|
+
)
|
|
172
|
+
response.raise_for_status()
|
|
173
|
+
data = response.json()
|
|
174
|
+
|
|
175
|
+
for repo_data in data.get("repositories", [])[:limit]:
|
|
176
|
+
repos.append(
|
|
177
|
+
Repository(
|
|
178
|
+
name=repo_data["name"],
|
|
179
|
+
owner=repo_data["owner"]["login"],
|
|
180
|
+
full_name=repo_data["full_name"],
|
|
181
|
+
html_url=repo_data["html_url"],
|
|
182
|
+
description=repo_data.get("description") or "",
|
|
183
|
+
default_branch=repo_data.get("default_branch", "main"),
|
|
184
|
+
private=repo_data.get("private", False),
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
except httpx.HTTPStatusError as e:
|
|
188
|
+
logger.error(
|
|
189
|
+
f"Failed to list installation repos: HTTP {e.response.status_code}"
|
|
190
|
+
)
|
|
191
|
+
raise GithubException(e.response.status_code, e.response.json())
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.error(f"Failed to list installation repos: {e}")
|
|
194
|
+
raise
|
|
195
|
+
|
|
196
|
+
return repos
|
|
197
|
+
|
|
198
|
+
def get_repo_info(
|
|
199
|
+
self, owner: Optional[str] = None, repo: Optional[str] = None
|
|
200
|
+
) -> Repository:
|
|
201
|
+
"""Get repository information."""
|
|
202
|
+
gh_repo = self._get_repo(owner, repo)
|
|
203
|
+
return Repository(
|
|
204
|
+
name=gh_repo.name,
|
|
205
|
+
owner=gh_repo.owner.login,
|
|
206
|
+
full_name=gh_repo.full_name,
|
|
207
|
+
html_url=gh_repo.html_url,
|
|
208
|
+
description=gh_repo.description or "",
|
|
209
|
+
default_branch=gh_repo.default_branch,
|
|
210
|
+
private=gh_repo.private,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# ========================================================================
|
|
214
|
+
# Pull Request Operations
|
|
215
|
+
# ========================================================================
|
|
216
|
+
|
|
217
|
+
def list_prs(
|
|
218
|
+
self,
|
|
219
|
+
owner: Optional[str] = None,
|
|
220
|
+
repo: Optional[str] = None,
|
|
221
|
+
state: str = "open",
|
|
222
|
+
limit: int = 20,
|
|
223
|
+
) -> List[PullRequest]:
|
|
224
|
+
"""
|
|
225
|
+
List pull requests.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
owner: Repository owner
|
|
229
|
+
repo: Repository name
|
|
230
|
+
state: PR state: 'open', 'closed', or 'all'
|
|
231
|
+
limit: Maximum PRs to return
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
List of pull requests
|
|
235
|
+
"""
|
|
236
|
+
gh_repo = self._get_repo(owner, repo)
|
|
237
|
+
prs = []
|
|
238
|
+
|
|
239
|
+
for pr in gh_repo.get_pulls(state=state, sort="updated", direction="desc")[
|
|
240
|
+
:limit
|
|
241
|
+
]:
|
|
242
|
+
prs.append(self._convert_pr(pr))
|
|
243
|
+
|
|
244
|
+
return prs
|
|
245
|
+
|
|
246
|
+
def get_pr(
|
|
247
|
+
self,
|
|
248
|
+
pr_number: int,
|
|
249
|
+
owner: Optional[str] = None,
|
|
250
|
+
repo: Optional[str] = None,
|
|
251
|
+
) -> Optional[PullRequest]:
|
|
252
|
+
"""
|
|
253
|
+
Get a specific pull request by number.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
pr_number: PR number
|
|
257
|
+
owner: Repository owner
|
|
258
|
+
repo: Repository name
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
PullRequest or None if not found
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
gh_repo = self._get_repo(owner, repo)
|
|
265
|
+
pr = gh_repo.get_pull(pr_number)
|
|
266
|
+
return self._convert_pr(pr)
|
|
267
|
+
except GithubException as e:
|
|
268
|
+
if e.status == 404:
|
|
269
|
+
return None
|
|
270
|
+
raise
|
|
271
|
+
|
|
272
|
+
def _convert_pr(self, pr) -> PullRequest:
|
|
273
|
+
"""Convert PyGithub PullRequest to Pydantic model."""
|
|
274
|
+
return PullRequest(
|
|
275
|
+
number=pr.number,
|
|
276
|
+
title=pr.title,
|
|
277
|
+
body=pr.body,
|
|
278
|
+
state=pr.state,
|
|
279
|
+
author=pr.user.login if pr.user else "unknown",
|
|
280
|
+
created_at=pr.created_at,
|
|
281
|
+
updated_at=pr.updated_at,
|
|
282
|
+
merged_at=pr.merged_at,
|
|
283
|
+
html_url=pr.html_url,
|
|
284
|
+
head_branch=pr.head.ref,
|
|
285
|
+
base_branch=pr.base.ref,
|
|
286
|
+
additions=pr.additions,
|
|
287
|
+
deletions=pr.deletions,
|
|
288
|
+
changed_files=pr.changed_files,
|
|
289
|
+
commits=pr.commits,
|
|
290
|
+
draft=pr.draft,
|
|
291
|
+
mergeable=pr.mergeable,
|
|
292
|
+
labels=[label.name for label in pr.labels],
|
|
293
|
+
reviewers=[r.login for r in pr.requested_reviewers],
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# ========================================================================
|
|
297
|
+
# Commit Operations
|
|
298
|
+
# ========================================================================
|
|
299
|
+
|
|
300
|
+
def list_commits(
|
|
301
|
+
self,
|
|
302
|
+
owner: Optional[str] = None,
|
|
303
|
+
repo: Optional[str] = None,
|
|
304
|
+
sha: Optional[str] = None,
|
|
305
|
+
author: Optional[str] = None,
|
|
306
|
+
since: Optional[str] = None,
|
|
307
|
+
limit: int = 20,
|
|
308
|
+
) -> List[Commit]:
|
|
309
|
+
"""
|
|
310
|
+
List commits.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
owner: Repository owner
|
|
314
|
+
repo: Repository name
|
|
315
|
+
sha: Branch name or commit SHA to start from
|
|
316
|
+
author: Filter by author username
|
|
317
|
+
since: ISO datetime - only commits after this date
|
|
318
|
+
limit: Maximum commits to return
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List of commits
|
|
322
|
+
"""
|
|
323
|
+
gh_repo = self._get_repo(owner, repo)
|
|
324
|
+
|
|
325
|
+
kwargs = {}
|
|
326
|
+
if sha:
|
|
327
|
+
kwargs["sha"] = sha
|
|
328
|
+
if since:
|
|
329
|
+
kwargs["since"] = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
|
330
|
+
|
|
331
|
+
commits = []
|
|
332
|
+
for commit in gh_repo.get_commits(**kwargs):
|
|
333
|
+
if len(commits) >= limit:
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
# Get author login
|
|
337
|
+
commit_author = "unknown"
|
|
338
|
+
if commit.author:
|
|
339
|
+
commit_author = commit.author.login
|
|
340
|
+
elif commit.commit.author:
|
|
341
|
+
commit_author = commit.commit.author.name
|
|
342
|
+
|
|
343
|
+
# Apply author filter
|
|
344
|
+
if author and author.lower() != commit_author.lower():
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
commits.append(
|
|
348
|
+
Commit(
|
|
349
|
+
sha=commit.sha,
|
|
350
|
+
message=commit.commit.message,
|
|
351
|
+
author=commit_author,
|
|
352
|
+
date=commit.commit.author.date,
|
|
353
|
+
html_url=commit.html_url,
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return commits
|
|
358
|
+
|
|
359
|
+
def get_commit(
|
|
360
|
+
self,
|
|
361
|
+
sha: str,
|
|
362
|
+
owner: Optional[str] = None,
|
|
363
|
+
repo: Optional[str] = None,
|
|
364
|
+
) -> Optional[Dict[str, Any]]:
|
|
365
|
+
"""
|
|
366
|
+
Get detailed commit information including file changes.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
sha: Commit SHA
|
|
370
|
+
owner: Repository owner
|
|
371
|
+
repo: Repository name
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Commit details with files or None if not found
|
|
375
|
+
"""
|
|
376
|
+
try:
|
|
377
|
+
gh_repo = self._get_repo(owner, repo)
|
|
378
|
+
commit = gh_repo.get_commit(sha)
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
"sha": commit.sha,
|
|
382
|
+
"message": commit.commit.message,
|
|
383
|
+
"author": commit.author.login if commit.author else "unknown",
|
|
384
|
+
"date": commit.commit.author.date.isoformat(),
|
|
385
|
+
"html_url": commit.html_url,
|
|
386
|
+
"stats": {
|
|
387
|
+
"additions": commit.stats.additions,
|
|
388
|
+
"deletions": commit.stats.deletions,
|
|
389
|
+
"total": commit.stats.total,
|
|
390
|
+
},
|
|
391
|
+
"files": [
|
|
392
|
+
{
|
|
393
|
+
"filename": f.filename,
|
|
394
|
+
"status": f.status,
|
|
395
|
+
"additions": f.additions,
|
|
396
|
+
"deletions": f.deletions,
|
|
397
|
+
"patch": f.patch[:1000] if f.patch else None,
|
|
398
|
+
}
|
|
399
|
+
for f in commit.files[:30] # Limit files
|
|
400
|
+
],
|
|
401
|
+
}
|
|
402
|
+
except GithubException as e:
|
|
403
|
+
if e.status == 404:
|
|
404
|
+
return None
|
|
405
|
+
raise
|
|
406
|
+
|
|
407
|
+
# ========================================================================
|
|
408
|
+
# Branch Operations
|
|
409
|
+
# ========================================================================
|
|
410
|
+
|
|
411
|
+
def list_branches(
|
|
412
|
+
self,
|
|
413
|
+
owner: Optional[str] = None,
|
|
414
|
+
repo: Optional[str] = None,
|
|
415
|
+
limit: int = 30,
|
|
416
|
+
) -> List[Dict[str, Any]]:
|
|
417
|
+
"""
|
|
418
|
+
List repository branches.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
owner: Repository owner
|
|
422
|
+
repo: Repository name
|
|
423
|
+
limit: Maximum branches to return
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
List of branch info dicts
|
|
427
|
+
"""
|
|
428
|
+
gh_repo = self._get_repo(owner, repo)
|
|
429
|
+
branches = []
|
|
430
|
+
|
|
431
|
+
for branch in gh_repo.get_branches()[:limit]:
|
|
432
|
+
branches.append(
|
|
433
|
+
{
|
|
434
|
+
"name": branch.name,
|
|
435
|
+
"sha": branch.commit.sha,
|
|
436
|
+
"protected": branch.protected,
|
|
437
|
+
}
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
return branches
|