|
c588255…
|
ragelink
|
1 |
"""Tool definitions and handlers for the fossilrepo MCP server. |
|
c588255…
|
ragelink
|
2 |
|
|
c588255…
|
ragelink
|
3 |
Each tool maps to a Fossil repository operation -- reads go through |
|
c588255…
|
ragelink
|
4 |
FossilReader (direct SQLite), writes go through FossilCLI (fossil binary). |
|
c588255…
|
ragelink
|
5 |
""" |
|
c588255…
|
ragelink
|
6 |
|
|
c588255…
|
ragelink
|
7 |
from mcp.types import Tool |
|
c588255…
|
ragelink
|
8 |
|
|
c588255…
|
ragelink
|
9 |
TOOLS = [ |
|
c588255…
|
ragelink
|
10 |
Tool( |
|
c588255…
|
ragelink
|
11 |
name="list_projects", |
|
c588255…
|
ragelink
|
12 |
description="List all projects in the fossilrepo instance", |
|
c588255…
|
ragelink
|
13 |
inputSchema={"type": "object", "properties": {}, "required": []}, |
|
c588255…
|
ragelink
|
14 |
), |
|
c588255…
|
ragelink
|
15 |
Tool( |
|
c588255…
|
ragelink
|
16 |
name="get_project", |
|
c588255…
|
ragelink
|
17 |
description="Get details about a specific project including repo stats", |
|
c588255…
|
ragelink
|
18 |
inputSchema={ |
|
c588255…
|
ragelink
|
19 |
"type": "object", |
|
c588255…
|
ragelink
|
20 |
"properties": {"slug": {"type": "string", "description": "Project slug"}}, |
|
c588255…
|
ragelink
|
21 |
"required": ["slug"], |
|
c588255…
|
ragelink
|
22 |
}, |
|
c588255…
|
ragelink
|
23 |
), |
|
c588255…
|
ragelink
|
24 |
Tool( |
|
c588255…
|
ragelink
|
25 |
name="browse_code", |
|
c588255…
|
ragelink
|
26 |
description="List files in a directory of a project's repository", |
|
c588255…
|
ragelink
|
27 |
inputSchema={ |
|
c588255…
|
ragelink
|
28 |
"type": "object", |
|
c588255…
|
ragelink
|
29 |
"properties": { |
|
c588255…
|
ragelink
|
30 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
31 |
"path": {"type": "string", "description": "Directory path (empty for root)", "default": ""}, |
|
c588255…
|
ragelink
|
32 |
}, |
|
c588255…
|
ragelink
|
33 |
"required": ["slug"], |
|
c588255…
|
ragelink
|
34 |
}, |
|
c588255…
|
ragelink
|
35 |
), |
|
c588255…
|
ragelink
|
36 |
Tool( |
|
c588255…
|
ragelink
|
37 |
name="read_file", |
|
c588255…
|
ragelink
|
38 |
description="Read the content of a file from a project's repository", |
|
c588255…
|
ragelink
|
39 |
inputSchema={ |
|
c588255…
|
ragelink
|
40 |
"type": "object", |
|
c588255…
|
ragelink
|
41 |
"properties": { |
|
c588255…
|
ragelink
|
42 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
43 |
"filepath": {"type": "string", "description": "File path in the repo"}, |
|
c588255…
|
ragelink
|
44 |
}, |
|
c588255…
|
ragelink
|
45 |
"required": ["slug", "filepath"], |
|
c588255…
|
ragelink
|
46 |
}, |
|
c588255…
|
ragelink
|
47 |
), |
|
c588255…
|
ragelink
|
48 |
Tool( |
|
c588255…
|
ragelink
|
49 |
name="get_timeline", |
|
c588255…
|
ragelink
|
50 |
description="Get recent checkins/commits for a project", |
|
c588255…
|
ragelink
|
51 |
inputSchema={ |
|
c588255…
|
ragelink
|
52 |
"type": "object", |
|
c588255…
|
ragelink
|
53 |
"properties": { |
|
c588255…
|
ragelink
|
54 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
55 |
"limit": {"type": "integer", "description": "Number of entries", "default": 25}, |
|
c588255…
|
ragelink
|
56 |
"branch": {"type": "string", "description": "Filter by branch", "default": ""}, |
|
c588255…
|
ragelink
|
57 |
}, |
|
c588255…
|
ragelink
|
58 |
"required": ["slug"], |
|
c588255…
|
ragelink
|
59 |
}, |
|
c588255…
|
ragelink
|
60 |
), |
|
c588255…
|
ragelink
|
61 |
Tool( |
|
c588255…
|
ragelink
|
62 |
name="get_checkin", |
|
c588255…
|
ragelink
|
63 |
description="Get details of a specific checkin including file changes", |
|
c588255…
|
ragelink
|
64 |
inputSchema={ |
|
c588255…
|
ragelink
|
65 |
"type": "object", |
|
c588255…
|
ragelink
|
66 |
"properties": { |
|
c588255…
|
ragelink
|
67 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
68 |
"uuid": {"type": "string", "description": "Checkin UUID (or prefix)"}, |
|
c588255…
|
ragelink
|
69 |
}, |
|
c588255…
|
ragelink
|
70 |
"required": ["slug", "uuid"], |
|
c588255…
|
ragelink
|
71 |
}, |
|
c588255…
|
ragelink
|
72 |
), |
|
c588255…
|
ragelink
|
73 |
Tool( |
|
c588255…
|
ragelink
|
74 |
name="search_code", |
|
c588255…
|
ragelink
|
75 |
description="Search across checkins, tickets, and wiki pages", |
|
c588255…
|
ragelink
|
76 |
inputSchema={ |
|
c588255…
|
ragelink
|
77 |
"type": "object", |
|
c588255…
|
ragelink
|
78 |
"properties": { |
|
c588255…
|
ragelink
|
79 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
80 |
"query": {"type": "string", "description": "Search query"}, |
|
c588255…
|
ragelink
|
81 |
"limit": {"type": "integer", "default": 25}, |
|
c588255…
|
ragelink
|
82 |
}, |
|
c588255…
|
ragelink
|
83 |
"required": ["slug", "query"], |
|
c588255…
|
ragelink
|
84 |
}, |
|
c588255…
|
ragelink
|
85 |
), |
|
c588255…
|
ragelink
|
86 |
Tool( |
|
c588255…
|
ragelink
|
87 |
name="list_tickets", |
|
c588255…
|
ragelink
|
88 |
description="List tickets for a project with optional status filter", |
|
c588255…
|
ragelink
|
89 |
inputSchema={ |
|
c588255…
|
ragelink
|
90 |
"type": "object", |
|
c588255…
|
ragelink
|
91 |
"properties": { |
|
c588255…
|
ragelink
|
92 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
93 |
"status": {"type": "string", "description": "Filter by status (Open, Fixed, Closed)", "default": ""}, |
|
c588255…
|
ragelink
|
94 |
"limit": {"type": "integer", "default": 50}, |
|
c588255…
|
ragelink
|
95 |
}, |
|
c588255…
|
ragelink
|
96 |
"required": ["slug"], |
|
c588255…
|
ragelink
|
97 |
}, |
|
c588255…
|
ragelink
|
98 |
), |
|
c588255…
|
ragelink
|
99 |
Tool( |
|
c588255…
|
ragelink
|
100 |
name="get_ticket", |
|
c588255…
|
ragelink
|
101 |
description="Get ticket details including comments", |
|
c588255…
|
ragelink
|
102 |
inputSchema={ |
|
c588255…
|
ragelink
|
103 |
"type": "object", |
|
c588255…
|
ragelink
|
104 |
"properties": { |
|
c588255…
|
ragelink
|
105 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
106 |
"uuid": {"type": "string", "description": "Ticket UUID (or prefix)"}, |
|
c588255…
|
ragelink
|
107 |
}, |
|
c588255…
|
ragelink
|
108 |
"required": ["slug", "uuid"], |
|
c588255…
|
ragelink
|
109 |
}, |
|
c588255…
|
ragelink
|
110 |
), |
|
c588255…
|
ragelink
|
111 |
Tool( |
|
c588255…
|
ragelink
|
112 |
name="create_ticket", |
|
c588255…
|
ragelink
|
113 |
description="Create a new ticket in a project", |
|
c588255…
|
ragelink
|
114 |
inputSchema={ |
|
c588255…
|
ragelink
|
115 |
"type": "object", |
|
c588255…
|
ragelink
|
116 |
"properties": { |
|
c588255…
|
ragelink
|
117 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
118 |
"title": {"type": "string"}, |
|
c588255…
|
ragelink
|
119 |
"body": {"type": "string", "description": "Ticket description"}, |
|
c588255…
|
ragelink
|
120 |
"type": {"type": "string", "default": "Code_Defect"}, |
|
c588255…
|
ragelink
|
121 |
"severity": {"type": "string", "default": "Important"}, |
|
c588255…
|
ragelink
|
122 |
"priority": {"type": "string", "default": "Medium"}, |
|
c588255…
|
ragelink
|
123 |
}, |
|
c588255…
|
ragelink
|
124 |
"required": ["slug", "title", "body"], |
|
c588255…
|
ragelink
|
125 |
}, |
|
c588255…
|
ragelink
|
126 |
), |
|
c588255…
|
ragelink
|
127 |
Tool( |
|
c588255…
|
ragelink
|
128 |
name="update_ticket", |
|
c588255…
|
ragelink
|
129 |
description="Update a ticket's status, add a comment", |
|
c588255…
|
ragelink
|
130 |
inputSchema={ |
|
c588255…
|
ragelink
|
131 |
"type": "object", |
|
c588255…
|
ragelink
|
132 |
"properties": { |
|
c588255…
|
ragelink
|
133 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
134 |
"uuid": {"type": "string", "description": "Ticket UUID"}, |
|
c588255…
|
ragelink
|
135 |
"status": {"type": "string", "description": "New status", "default": ""}, |
|
c588255…
|
ragelink
|
136 |
"comment": {"type": "string", "description": "Comment to add", "default": ""}, |
|
c588255…
|
ragelink
|
137 |
}, |
|
c588255…
|
ragelink
|
138 |
"required": ["slug", "uuid"], |
|
c588255…
|
ragelink
|
139 |
}, |
|
c588255…
|
ragelink
|
140 |
), |
|
c588255…
|
ragelink
|
141 |
Tool( |
|
c588255…
|
ragelink
|
142 |
name="list_wiki_pages", |
|
c588255…
|
ragelink
|
143 |
description="List all wiki pages in a project", |
|
c588255…
|
ragelink
|
144 |
inputSchema={ |
|
c588255…
|
ragelink
|
145 |
"type": "object", |
|
c588255…
|
ragelink
|
146 |
"properties": {"slug": {"type": "string", "description": "Project slug"}}, |
|
c588255…
|
ragelink
|
147 |
"required": ["slug"], |
|
c588255…
|
ragelink
|
148 |
}, |
|
c588255…
|
ragelink
|
149 |
), |
|
c588255…
|
ragelink
|
150 |
Tool( |
|
c588255…
|
ragelink
|
151 |
name="get_wiki_page", |
|
c588255…
|
ragelink
|
152 |
description="Read a wiki page's content", |
|
c588255…
|
ragelink
|
153 |
inputSchema={ |
|
c588255…
|
ragelink
|
154 |
"type": "object", |
|
c588255…
|
ragelink
|
155 |
"properties": { |
|
c588255…
|
ragelink
|
156 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
157 |
"page_name": {"type": "string", "description": "Wiki page name"}, |
|
c588255…
|
ragelink
|
158 |
}, |
|
c588255…
|
ragelink
|
159 |
"required": ["slug", "page_name"], |
|
c588255…
|
ragelink
|
160 |
}, |
|
c588255…
|
ragelink
|
161 |
), |
|
c588255…
|
ragelink
|
162 |
Tool( |
|
c588255…
|
ragelink
|
163 |
name="list_branches", |
|
c588255…
|
ragelink
|
164 |
description="List all branches in a project's repository", |
|
c588255…
|
ragelink
|
165 |
inputSchema={ |
|
c588255…
|
ragelink
|
166 |
"type": "object", |
|
c588255…
|
ragelink
|
167 |
"properties": {"slug": {"type": "string", "description": "Project slug"}}, |
|
c588255…
|
ragelink
|
168 |
"required": ["slug"], |
|
c588255…
|
ragelink
|
169 |
}, |
|
c588255…
|
ragelink
|
170 |
), |
|
c588255…
|
ragelink
|
171 |
Tool( |
|
c588255…
|
ragelink
|
172 |
name="get_file_blame", |
|
c588255…
|
ragelink
|
173 |
description="Get blame annotations for a file showing who changed each line", |
|
c588255…
|
ragelink
|
174 |
inputSchema={ |
|
c588255…
|
ragelink
|
175 |
"type": "object", |
|
c588255…
|
ragelink
|
176 |
"properties": { |
|
c588255…
|
ragelink
|
177 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
178 |
"filepath": {"type": "string", "description": "File path"}, |
|
c588255…
|
ragelink
|
179 |
}, |
|
c588255…
|
ragelink
|
180 |
"required": ["slug", "filepath"], |
|
c588255…
|
ragelink
|
181 |
}, |
|
c588255…
|
ragelink
|
182 |
), |
|
c588255…
|
ragelink
|
183 |
Tool( |
|
c588255…
|
ragelink
|
184 |
name="get_file_history", |
|
c588255…
|
ragelink
|
185 |
description="Get commit history for a specific file", |
|
c588255…
|
ragelink
|
186 |
inputSchema={ |
|
c588255…
|
ragelink
|
187 |
"type": "object", |
|
c588255…
|
ragelink
|
188 |
"properties": { |
|
c588255…
|
ragelink
|
189 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
190 |
"filepath": {"type": "string", "description": "File path"}, |
|
c588255…
|
ragelink
|
191 |
"limit": {"type": "integer", "default": 25}, |
|
c588255…
|
ragelink
|
192 |
}, |
|
c588255…
|
ragelink
|
193 |
"required": ["slug", "filepath"], |
|
c588255…
|
ragelink
|
194 |
}, |
|
c588255…
|
ragelink
|
195 |
), |
|
c588255…
|
ragelink
|
196 |
Tool( |
|
c588255…
|
ragelink
|
197 |
name="sql_query", |
|
c588255…
|
ragelink
|
198 |
description="Run a read-only SQL query against the Fossil SQLite database. Only SELECT allowed.", |
|
c588255…
|
ragelink
|
199 |
inputSchema={ |
|
c588255…
|
ragelink
|
200 |
"type": "object", |
|
c588255…
|
ragelink
|
201 |
"properties": { |
|
c588255…
|
ragelink
|
202 |
"slug": {"type": "string", "description": "Project slug"}, |
|
c588255…
|
ragelink
|
203 |
"sql": {"type": "string", "description": "SQL query (SELECT only)"}, |
|
c588255…
|
ragelink
|
204 |
}, |
|
c588255…
|
ragelink
|
205 |
"required": ["slug", "sql"], |
|
c588255…
|
ragelink
|
206 |
}, |
|
c588255…
|
ragelink
|
207 |
), |
|
c588255…
|
ragelink
|
208 |
] |
|
c588255…
|
ragelink
|
209 |
|
|
c588255…
|
ragelink
|
210 |
|
|
c588255…
|
ragelink
|
211 |
def _isoformat(dt): |
|
c588255…
|
ragelink
|
212 |
"""Safely format a datetime to ISO 8601, or None.""" |
|
c588255…
|
ragelink
|
213 |
if dt is None: |
|
c588255…
|
ragelink
|
214 |
return None |
|
c588255…
|
ragelink
|
215 |
return dt.isoformat() |
|
c588255…
|
ragelink
|
216 |
|
|
c588255…
|
ragelink
|
217 |
|
|
c588255…
|
ragelink
|
218 |
def _get_repo(slug): |
|
c588255…
|
ragelink
|
219 |
"""Look up project and its FossilRepository by slug. |
|
c588255…
|
ragelink
|
220 |
|
|
c588255…
|
ragelink
|
221 |
Raises Project.DoesNotExist or FossilRepository.DoesNotExist on miss. |
|
c588255…
|
ragelink
|
222 |
""" |
|
c588255…
|
ragelink
|
223 |
from fossil.models import FossilRepository |
|
c588255…
|
ragelink
|
224 |
from projects.models import Project |
|
c588255…
|
ragelink
|
225 |
|
|
c588255…
|
ragelink
|
226 |
project = Project.objects.get(slug=slug, deleted_at__isnull=True) |
|
c588255…
|
ragelink
|
227 |
repo = FossilRepository.objects.get(project=project, deleted_at__isnull=True) |
|
c588255…
|
ragelink
|
228 |
return project, repo |
|
c588255…
|
ragelink
|
229 |
|
|
c588255…
|
ragelink
|
230 |
|
|
c588255…
|
ragelink
|
231 |
def execute_tool(name: str, arguments: dict) -> dict: |
|
c588255…
|
ragelink
|
232 |
"""Dispatch a tool call to the appropriate handler.""" |
|
c588255…
|
ragelink
|
233 |
handlers = { |
|
c588255…
|
ragelink
|
234 |
"list_projects": _list_projects, |
|
c588255…
|
ragelink
|
235 |
"get_project": _get_project, |
|
c588255…
|
ragelink
|
236 |
"browse_code": _browse_code, |
|
c588255…
|
ragelink
|
237 |
"read_file": _read_file, |
|
c588255…
|
ragelink
|
238 |
"get_timeline": _get_timeline, |
|
c588255…
|
ragelink
|
239 |
"get_checkin": _get_checkin, |
|
c588255…
|
ragelink
|
240 |
"search_code": _search_code, |
|
c588255…
|
ragelink
|
241 |
"list_tickets": _list_tickets, |
|
c588255…
|
ragelink
|
242 |
"get_ticket": _get_ticket, |
|
c588255…
|
ragelink
|
243 |
"create_ticket": _create_ticket, |
|
c588255…
|
ragelink
|
244 |
"update_ticket": _update_ticket, |
|
c588255…
|
ragelink
|
245 |
"list_wiki_pages": _list_wiki_pages, |
|
c588255…
|
ragelink
|
246 |
"get_wiki_page": _get_wiki_page, |
|
c588255…
|
ragelink
|
247 |
"list_branches": _list_branches, |
|
c588255…
|
ragelink
|
248 |
"get_file_blame": _get_file_blame, |
|
c588255…
|
ragelink
|
249 |
"get_file_history": _get_file_history, |
|
c588255…
|
ragelink
|
250 |
"sql_query": _sql_query, |
|
c588255…
|
ragelink
|
251 |
} |
|
c588255…
|
ragelink
|
252 |
handler = handlers.get(name) |
|
c588255…
|
ragelink
|
253 |
if not handler: |
|
c588255…
|
ragelink
|
254 |
return {"error": f"Unknown tool: {name}"} |
|
c588255…
|
ragelink
|
255 |
try: |
|
c588255…
|
ragelink
|
256 |
return handler(arguments) |
|
c588255…
|
ragelink
|
257 |
except Exception as e: |
|
c588255…
|
ragelink
|
258 |
return {"error": str(e)} |
|
c588255…
|
ragelink
|
259 |
|
|
c588255…
|
ragelink
|
260 |
|
|
c588255…
|
ragelink
|
261 |
# --------------------------------------------------------------------------- |
|
c588255…
|
ragelink
|
262 |
# Read-only handlers (FossilReader) |
|
c588255…
|
ragelink
|
263 |
# --------------------------------------------------------------------------- |
|
c588255…
|
ragelink
|
264 |
|
|
c588255…
|
ragelink
|
265 |
|
|
c588255…
|
ragelink
|
266 |
def _list_projects(args): |
|
c588255…
|
ragelink
|
267 |
from projects.models import Project |
|
c588255…
|
ragelink
|
268 |
|
|
c588255…
|
ragelink
|
269 |
projects = Project.objects.filter(deleted_at__isnull=True) |
|
c588255…
|
ragelink
|
270 |
return { |
|
c588255…
|
ragelink
|
271 |
"projects": [ |
|
c588255…
|
ragelink
|
272 |
{ |
|
c588255…
|
ragelink
|
273 |
"name": p.name, |
|
c588255…
|
ragelink
|
274 |
"slug": p.slug, |
|
c588255…
|
ragelink
|
275 |
"description": p.description or "", |
|
c588255…
|
ragelink
|
276 |
"visibility": p.visibility, |
|
c588255…
|
ragelink
|
277 |
} |
|
c588255…
|
ragelink
|
278 |
for p in projects |
|
c588255…
|
ragelink
|
279 |
] |
|
c588255…
|
ragelink
|
280 |
} |
|
c588255…
|
ragelink
|
281 |
|
|
c588255…
|
ragelink
|
282 |
|
|
c588255…
|
ragelink
|
283 |
def _get_project(args): |
|
c588255…
|
ragelink
|
284 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
285 |
|
|
c588255…
|
ragelink
|
286 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
287 |
result = { |
|
c588255…
|
ragelink
|
288 |
"name": project.name, |
|
c588255…
|
ragelink
|
289 |
"slug": project.slug, |
|
c588255…
|
ragelink
|
290 |
"description": project.description or "", |
|
c588255…
|
ragelink
|
291 |
"visibility": project.visibility, |
|
c588255…
|
ragelink
|
292 |
"star_count": project.star_count, |
|
c588255…
|
ragelink
|
293 |
"filename": repo.filename, |
|
c588255…
|
ragelink
|
294 |
"file_size_bytes": repo.file_size_bytes, |
|
c588255…
|
ragelink
|
295 |
"checkin_count": repo.checkin_count, |
|
c588255…
|
ragelink
|
296 |
"last_checkin_at": _isoformat(repo.last_checkin_at), |
|
c588255…
|
ragelink
|
297 |
} |
|
c588255…
|
ragelink
|
298 |
if repo.exists_on_disk: |
|
c588255…
|
ragelink
|
299 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
300 |
meta = reader.get_metadata() |
|
c588255…
|
ragelink
|
301 |
result["fossil_project_name"] = meta.project_name |
|
c588255…
|
ragelink
|
302 |
result["fossil_checkin_count"] = meta.checkin_count |
|
c588255…
|
ragelink
|
303 |
result["fossil_ticket_count"] = meta.ticket_count |
|
c588255…
|
ragelink
|
304 |
result["fossil_wiki_page_count"] = meta.wiki_page_count |
|
c588255…
|
ragelink
|
305 |
return result |
|
c588255…
|
ragelink
|
306 |
|
|
c588255…
|
ragelink
|
307 |
|
|
c588255…
|
ragelink
|
308 |
def _browse_code(args): |
|
c588255…
|
ragelink
|
309 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
310 |
|
|
c588255…
|
ragelink
|
311 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
312 |
path = args.get("path", "") |
|
c588255…
|
ragelink
|
313 |
|
|
c588255…
|
ragelink
|
314 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
315 |
checkin = reader.get_latest_checkin_uuid() |
|
c588255…
|
ragelink
|
316 |
if not checkin: |
|
c588255…
|
ragelink
|
317 |
return {"files": [], "error": "No checkins in repository"} |
|
c588255…
|
ragelink
|
318 |
|
|
c588255…
|
ragelink
|
319 |
files = reader.get_files_at_checkin(checkin) |
|
c588255…
|
ragelink
|
320 |
|
|
c588255…
|
ragelink
|
321 |
# Filter to requested directory |
|
c588255…
|
ragelink
|
322 |
if path: |
|
c588255…
|
ragelink
|
323 |
path = path.rstrip("/") + "/" |
|
c588255…
|
ragelink
|
324 |
files = [f for f in files if f.name.startswith(path)] |
|
c588255…
|
ragelink
|
325 |
|
|
c588255…
|
ragelink
|
326 |
return { |
|
c588255…
|
ragelink
|
327 |
"checkin": checkin, |
|
c588255…
|
ragelink
|
328 |
"path": path, |
|
c588255…
|
ragelink
|
329 |
"files": [ |
|
c588255…
|
ragelink
|
330 |
{ |
|
c588255…
|
ragelink
|
331 |
"name": f.name, |
|
c588255…
|
ragelink
|
332 |
"uuid": f.uuid, |
|
c588255…
|
ragelink
|
333 |
"size": f.size, |
|
c588255…
|
ragelink
|
334 |
"last_commit_message": f.last_commit_message, |
|
c588255…
|
ragelink
|
335 |
"last_commit_user": f.last_commit_user, |
|
c588255…
|
ragelink
|
336 |
"last_commit_time": _isoformat(f.last_commit_time), |
|
c588255…
|
ragelink
|
337 |
} |
|
c588255…
|
ragelink
|
338 |
for f in files |
|
c588255…
|
ragelink
|
339 |
], |
|
c588255…
|
ragelink
|
340 |
} |
|
c588255…
|
ragelink
|
341 |
|
|
c588255…
|
ragelink
|
342 |
|
|
c588255…
|
ragelink
|
343 |
def _read_file(args): |
|
c588255…
|
ragelink
|
344 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
345 |
|
|
c588255…
|
ragelink
|
346 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
347 |
|
|
c588255…
|
ragelink
|
348 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
349 |
checkin = reader.get_latest_checkin_uuid() |
|
c588255…
|
ragelink
|
350 |
if not checkin: |
|
c588255…
|
ragelink
|
351 |
return {"error": "No checkins in repository"} |
|
c588255…
|
ragelink
|
352 |
|
|
c588255…
|
ragelink
|
353 |
files = reader.get_files_at_checkin(checkin) |
|
c588255…
|
ragelink
|
354 |
target = args["filepath"] |
|
c588255…
|
ragelink
|
355 |
|
|
c588255…
|
ragelink
|
356 |
for f in files: |
|
c588255…
|
ragelink
|
357 |
if f.name == target: |
|
c588255…
|
ragelink
|
358 |
content = reader.get_file_content(f.uuid) |
|
c588255…
|
ragelink
|
359 |
if isinstance(content, bytes): |
|
c588255…
|
ragelink
|
360 |
try: |
|
c588255…
|
ragelink
|
361 |
content = content.decode("utf-8") |
|
c588255…
|
ragelink
|
362 |
except UnicodeDecodeError: |
|
c588255…
|
ragelink
|
363 |
return {"filepath": target, "binary": True, "size": len(content)} |
|
c588255…
|
ragelink
|
364 |
return {"filepath": target, "content": content} |
|
c588255…
|
ragelink
|
365 |
|
|
c588255…
|
ragelink
|
366 |
return {"error": f"File not found: {target}"} |
|
c588255…
|
ragelink
|
367 |
|
|
c588255…
|
ragelink
|
368 |
|
|
c588255…
|
ragelink
|
369 |
def _get_timeline(args): |
|
c588255…
|
ragelink
|
370 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
371 |
|
|
c588255…
|
ragelink
|
372 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
373 |
limit = args.get("limit", 25) |
|
c588255…
|
ragelink
|
374 |
branch_filter = args.get("branch", "") |
|
c588255…
|
ragelink
|
375 |
|
|
c588255…
|
ragelink
|
376 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
377 |
entries = reader.get_timeline(limit=limit, event_type="ci") |
|
c588255…
|
ragelink
|
378 |
|
|
c588255…
|
ragelink
|
379 |
checkins = [] |
|
c588255…
|
ragelink
|
380 |
for e in entries: |
|
c588255…
|
ragelink
|
381 |
entry = { |
|
c588255…
|
ragelink
|
382 |
"uuid": e.uuid, |
|
c588255…
|
ragelink
|
383 |
"timestamp": _isoformat(e.timestamp), |
|
c588255…
|
ragelink
|
384 |
"user": e.user, |
|
c588255…
|
ragelink
|
385 |
"comment": e.comment, |
|
c588255…
|
ragelink
|
386 |
"branch": e.branch, |
|
c588255…
|
ragelink
|
387 |
} |
|
c588255…
|
ragelink
|
388 |
checkins.append(entry) |
|
c588255…
|
ragelink
|
389 |
|
|
c588255…
|
ragelink
|
390 |
if branch_filter: |
|
c588255…
|
ragelink
|
391 |
checkins = [c for c in checkins if c["branch"] == branch_filter] |
|
c588255…
|
ragelink
|
392 |
|
|
c588255…
|
ragelink
|
393 |
return {"checkins": checkins, "total": len(checkins)} |
|
c588255…
|
ragelink
|
394 |
|
|
c588255…
|
ragelink
|
395 |
|
|
c588255…
|
ragelink
|
396 |
def _get_checkin(args): |
|
c588255…
|
ragelink
|
397 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
398 |
|
|
c588255…
|
ragelink
|
399 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
400 |
|
|
c588255…
|
ragelink
|
401 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
402 |
detail = reader.get_checkin_detail(args["uuid"]) |
|
c588255…
|
ragelink
|
403 |
|
|
c588255…
|
ragelink
|
404 |
if detail is None: |
|
c588255…
|
ragelink
|
405 |
return {"error": "Checkin not found"} |
|
c588255…
|
ragelink
|
406 |
|
|
c588255…
|
ragelink
|
407 |
return { |
|
c588255…
|
ragelink
|
408 |
"uuid": detail.uuid, |
|
c588255…
|
ragelink
|
409 |
"timestamp": _isoformat(detail.timestamp), |
|
c588255…
|
ragelink
|
410 |
"user": detail.user, |
|
c588255…
|
ragelink
|
411 |
"comment": detail.comment, |
|
c588255…
|
ragelink
|
412 |
"branch": detail.branch, |
|
c588255…
|
ragelink
|
413 |
"parent_uuid": detail.parent_uuid, |
|
c588255…
|
ragelink
|
414 |
"is_merge": detail.is_merge, |
|
c588255…
|
ragelink
|
415 |
"files_changed": detail.files_changed, |
|
c588255…
|
ragelink
|
416 |
} |
|
c588255…
|
ragelink
|
417 |
|
|
c588255…
|
ragelink
|
418 |
|
|
c588255…
|
ragelink
|
419 |
def _search_code(args): |
|
c588255…
|
ragelink
|
420 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
421 |
|
|
c588255…
|
ragelink
|
422 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
423 |
query = args["query"] |
|
c588255…
|
ragelink
|
424 |
limit = args.get("limit", 25) |
|
c588255…
|
ragelink
|
425 |
|
|
c588255…
|
ragelink
|
426 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
427 |
results = reader.search(query, limit=limit) |
|
c588255…
|
ragelink
|
428 |
|
|
c588255…
|
ragelink
|
429 |
# Serialize datetimes in results |
|
c588255…
|
ragelink
|
430 |
for checkin in results.get("checkins", []): |
|
c588255…
|
ragelink
|
431 |
checkin["timestamp"] = _isoformat(checkin.get("timestamp")) |
|
c588255…
|
ragelink
|
432 |
for ticket in results.get("tickets", []): |
|
c588255…
|
ragelink
|
433 |
ticket["created"] = _isoformat(ticket.get("created")) |
|
c588255…
|
ragelink
|
434 |
|
|
c588255…
|
ragelink
|
435 |
return results |
|
c588255…
|
ragelink
|
436 |
|
|
c588255…
|
ragelink
|
437 |
|
|
c588255…
|
ragelink
|
438 |
def _list_tickets(args): |
|
c588255…
|
ragelink
|
439 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
440 |
|
|
c588255…
|
ragelink
|
441 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
442 |
status_filter = args.get("status", "") or None |
|
c588255…
|
ragelink
|
443 |
limit = args.get("limit", 50) |
|
c588255…
|
ragelink
|
444 |
|
|
c588255…
|
ragelink
|
445 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
446 |
tickets = reader.get_tickets(status=status_filter, limit=limit) |
|
c588255…
|
ragelink
|
447 |
|
|
c588255…
|
ragelink
|
448 |
return { |
|
c588255…
|
ragelink
|
449 |
"tickets": [ |
|
c588255…
|
ragelink
|
450 |
{ |
|
c588255…
|
ragelink
|
451 |
"uuid": t.uuid, |
|
c588255…
|
ragelink
|
452 |
"title": t.title, |
|
c588255…
|
ragelink
|
453 |
"status": t.status, |
|
c588255…
|
ragelink
|
454 |
"type": t.type, |
|
c588255…
|
ragelink
|
455 |
"subsystem": t.subsystem, |
|
c588255…
|
ragelink
|
456 |
"priority": t.priority, |
|
c588255…
|
ragelink
|
457 |
"created": _isoformat(t.created), |
|
c588255…
|
ragelink
|
458 |
} |
|
c588255…
|
ragelink
|
459 |
for t in tickets |
|
c588255…
|
ragelink
|
460 |
], |
|
c588255…
|
ragelink
|
461 |
"total": len(tickets), |
|
c588255…
|
ragelink
|
462 |
} |
|
c588255…
|
ragelink
|
463 |
|
|
c588255…
|
ragelink
|
464 |
|
|
c588255…
|
ragelink
|
465 |
def _get_ticket(args): |
|
c588255…
|
ragelink
|
466 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
467 |
|
|
c588255…
|
ragelink
|
468 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
469 |
|
|
c588255…
|
ragelink
|
470 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
471 |
ticket = reader.get_ticket_detail(args["uuid"]) |
|
c588255…
|
ragelink
|
472 |
if ticket is None: |
|
c588255…
|
ragelink
|
473 |
return {"error": "Ticket not found"} |
|
c588255…
|
ragelink
|
474 |
comments = reader.get_ticket_comments(args["uuid"]) |
|
c588255…
|
ragelink
|
475 |
|
|
c588255…
|
ragelink
|
476 |
return { |
|
c588255…
|
ragelink
|
477 |
"uuid": ticket.uuid, |
|
c588255…
|
ragelink
|
478 |
"title": ticket.title, |
|
c588255…
|
ragelink
|
479 |
"status": ticket.status, |
|
c588255…
|
ragelink
|
480 |
"type": ticket.type, |
|
c588255…
|
ragelink
|
481 |
"subsystem": ticket.subsystem, |
|
c588255…
|
ragelink
|
482 |
"priority": ticket.priority, |
|
c588255…
|
ragelink
|
483 |
"severity": ticket.severity, |
|
c588255…
|
ragelink
|
484 |
"resolution": ticket.resolution, |
|
c588255…
|
ragelink
|
485 |
"body": ticket.body, |
|
c588255…
|
ragelink
|
486 |
"created": _isoformat(ticket.created), |
|
c588255…
|
ragelink
|
487 |
"comments": [ |
|
c588255…
|
ragelink
|
488 |
{ |
|
c588255…
|
ragelink
|
489 |
"timestamp": _isoformat(c.get("timestamp")), |
|
c588255…
|
ragelink
|
490 |
"user": c.get("user", ""), |
|
c588255…
|
ragelink
|
491 |
"comment": c.get("comment", ""), |
|
c588255…
|
ragelink
|
492 |
"mimetype": c.get("mimetype", "text/plain"), |
|
c588255…
|
ragelink
|
493 |
} |
|
c588255…
|
ragelink
|
494 |
for c in comments |
|
c588255…
|
ragelink
|
495 |
], |
|
c588255…
|
ragelink
|
496 |
} |
|
c588255…
|
ragelink
|
497 |
|
|
c588255…
|
ragelink
|
498 |
|
|
c588255…
|
ragelink
|
499 |
# --------------------------------------------------------------------------- |
|
c588255…
|
ragelink
|
500 |
# Write handlers (FossilCLI) |
|
c588255…
|
ragelink
|
501 |
# --------------------------------------------------------------------------- |
|
c588255…
|
ragelink
|
502 |
|
|
c588255…
|
ragelink
|
503 |
|
|
c588255…
|
ragelink
|
504 |
def _create_ticket(args): |
|
c588255…
|
ragelink
|
505 |
from fossil.cli import FossilCLI |
|
c588255…
|
ragelink
|
506 |
|
|
c588255…
|
ragelink
|
507 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
508 |
|
|
c588255…
|
ragelink
|
509 |
cli = FossilCLI() |
|
c588255…
|
ragelink
|
510 |
cli.ensure_default_user(repo.full_path) |
|
c588255…
|
ragelink
|
511 |
|
|
c588255…
|
ragelink
|
512 |
fields = { |
|
c588255…
|
ragelink
|
513 |
"title": args["title"], |
|
c588255…
|
ragelink
|
514 |
"comment": args["body"], |
|
c588255…
|
ragelink
|
515 |
"type": args.get("type", "Code_Defect"), |
|
c588255…
|
ragelink
|
516 |
"severity": args.get("severity", "Important"), |
|
c588255…
|
ragelink
|
517 |
"priority": args.get("priority", "Medium"), |
|
c588255…
|
ragelink
|
518 |
"status": "Open", |
|
c588255…
|
ragelink
|
519 |
} |
|
c588255…
|
ragelink
|
520 |
|
|
c588255…
|
ragelink
|
521 |
success = cli.ticket_add(repo.full_path, fields) |
|
c588255…
|
ragelink
|
522 |
if not success: |
|
c588255…
|
ragelink
|
523 |
return {"error": "Failed to create ticket"} |
|
c588255…
|
ragelink
|
524 |
|
|
c588255…
|
ragelink
|
525 |
return {"success": True, "title": args["title"]} |
|
c588255…
|
ragelink
|
526 |
|
|
c588255…
|
ragelink
|
527 |
|
|
c588255…
|
ragelink
|
528 |
def _update_ticket(args): |
|
c588255…
|
ragelink
|
529 |
from fossil.cli import FossilCLI |
|
c588255…
|
ragelink
|
530 |
|
|
c588255…
|
ragelink
|
531 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
532 |
|
|
c588255…
|
ragelink
|
533 |
cli = FossilCLI() |
|
c588255…
|
ragelink
|
534 |
cli.ensure_default_user(repo.full_path) |
|
c588255…
|
ragelink
|
535 |
|
|
c588255…
|
ragelink
|
536 |
fields = {} |
|
c588255…
|
ragelink
|
537 |
if args.get("status"): |
|
c588255…
|
ragelink
|
538 |
fields["status"] = args["status"] |
|
c588255…
|
ragelink
|
539 |
if args.get("comment"): |
|
c588255…
|
ragelink
|
540 |
fields["icomment"] = args["comment"] |
|
c588255…
|
ragelink
|
541 |
|
|
c588255…
|
ragelink
|
542 |
if not fields: |
|
c588255…
|
ragelink
|
543 |
return {"error": "No fields to update (provide status or comment)"} |
|
c588255…
|
ragelink
|
544 |
|
|
c588255…
|
ragelink
|
545 |
success = cli.ticket_change(repo.full_path, args["uuid"], fields) |
|
c588255…
|
ragelink
|
546 |
if not success: |
|
c588255…
|
ragelink
|
547 |
return {"error": "Failed to update ticket"} |
|
c588255…
|
ragelink
|
548 |
|
|
c588255…
|
ragelink
|
549 |
return {"success": True, "uuid": args["uuid"]} |
|
c588255…
|
ragelink
|
550 |
|
|
c588255…
|
ragelink
|
551 |
|
|
c588255…
|
ragelink
|
552 |
# --------------------------------------------------------------------------- |
|
c588255…
|
ragelink
|
553 |
# Wiki handlers (FossilReader for reads) |
|
c588255…
|
ragelink
|
554 |
# --------------------------------------------------------------------------- |
|
c588255…
|
ragelink
|
555 |
|
|
c588255…
|
ragelink
|
556 |
|
|
c588255…
|
ragelink
|
557 |
def _list_wiki_pages(args): |
|
c588255…
|
ragelink
|
558 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
559 |
|
|
c588255…
|
ragelink
|
560 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
561 |
|
|
c588255…
|
ragelink
|
562 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
563 |
pages = reader.get_wiki_pages() |
|
c588255…
|
ragelink
|
564 |
|
|
c588255…
|
ragelink
|
565 |
return { |
|
c588255…
|
ragelink
|
566 |
"pages": [ |
|
c588255…
|
ragelink
|
567 |
{ |
|
c588255…
|
ragelink
|
568 |
"name": p.name, |
|
c588255…
|
ragelink
|
569 |
"last_modified": _isoformat(p.last_modified), |
|
c588255…
|
ragelink
|
570 |
"user": p.user, |
|
c588255…
|
ragelink
|
571 |
} |
|
c588255…
|
ragelink
|
572 |
for p in pages |
|
c588255…
|
ragelink
|
573 |
] |
|
c588255…
|
ragelink
|
574 |
} |
|
c588255…
|
ragelink
|
575 |
|
|
c588255…
|
ragelink
|
576 |
|
|
c588255…
|
ragelink
|
577 |
def _get_wiki_page(args): |
|
c588255…
|
ragelink
|
578 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
579 |
|
|
c588255…
|
ragelink
|
580 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
581 |
|
|
c588255…
|
ragelink
|
582 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
583 |
page = reader.get_wiki_page(args["page_name"]) |
|
c588255…
|
ragelink
|
584 |
|
|
c588255…
|
ragelink
|
585 |
if page is None: |
|
c588255…
|
ragelink
|
586 |
return {"error": f"Wiki page not found: {args['page_name']}"} |
|
c588255…
|
ragelink
|
587 |
|
|
c588255…
|
ragelink
|
588 |
return { |
|
c588255…
|
ragelink
|
589 |
"name": page.name, |
|
c588255…
|
ragelink
|
590 |
"content": page.content, |
|
c588255…
|
ragelink
|
591 |
"last_modified": _isoformat(page.last_modified), |
|
c588255…
|
ragelink
|
592 |
"user": page.user, |
|
c588255…
|
ragelink
|
593 |
} |
|
c588255…
|
ragelink
|
594 |
|
|
c588255…
|
ragelink
|
595 |
|
|
c588255…
|
ragelink
|
596 |
# --------------------------------------------------------------------------- |
|
c588255…
|
ragelink
|
597 |
# Branch and file history handlers |
|
c588255…
|
ragelink
|
598 |
# --------------------------------------------------------------------------- |
|
c588255…
|
ragelink
|
599 |
|
|
c588255…
|
ragelink
|
600 |
|
|
c588255…
|
ragelink
|
601 |
def _list_branches(args): |
|
c588255…
|
ragelink
|
602 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
603 |
|
|
c588255…
|
ragelink
|
604 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
605 |
|
|
c588255…
|
ragelink
|
606 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
607 |
branches = reader.get_branches() |
|
c588255…
|
ragelink
|
608 |
|
|
c588255…
|
ragelink
|
609 |
return { |
|
c588255…
|
ragelink
|
610 |
"branches": [ |
|
c588255…
|
ragelink
|
611 |
{ |
|
c588255…
|
ragelink
|
612 |
"name": b["name"], |
|
c588255…
|
ragelink
|
613 |
"last_checkin": _isoformat(b["last_checkin"]), |
|
c588255…
|
ragelink
|
614 |
"last_user": b["last_user"], |
|
c588255…
|
ragelink
|
615 |
"checkin_count": b["checkin_count"], |
|
c588255…
|
ragelink
|
616 |
"last_uuid": b["last_uuid"], |
|
c588255…
|
ragelink
|
617 |
} |
|
c588255…
|
ragelink
|
618 |
for b in branches |
|
c588255…
|
ragelink
|
619 |
] |
|
c588255…
|
ragelink
|
620 |
} |
|
c588255…
|
ragelink
|
621 |
|
|
c588255…
|
ragelink
|
622 |
|
|
c588255…
|
ragelink
|
623 |
def _get_file_blame(args): |
|
c588255…
|
ragelink
|
624 |
from fossil.cli import FossilCLI |
|
c588255…
|
ragelink
|
625 |
|
|
c588255…
|
ragelink
|
626 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
627 |
|
|
c588255…
|
ragelink
|
628 |
cli = FossilCLI() |
|
c588255…
|
ragelink
|
629 |
lines = cli.blame(repo.full_path, args["filepath"]) |
|
c588255…
|
ragelink
|
630 |
return {"filepath": args["filepath"], "lines": lines, "total": len(lines)} |
|
c588255…
|
ragelink
|
631 |
|
|
c588255…
|
ragelink
|
632 |
|
|
c588255…
|
ragelink
|
633 |
def _get_file_history(args): |
|
c588255…
|
ragelink
|
634 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
635 |
|
|
c588255…
|
ragelink
|
636 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
637 |
limit = args.get("limit", 25) |
|
c588255…
|
ragelink
|
638 |
|
|
c588255…
|
ragelink
|
639 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
640 |
history = reader.get_file_history(args["filepath"], limit=limit) |
|
c588255…
|
ragelink
|
641 |
|
|
c588255…
|
ragelink
|
642 |
for entry in history: |
|
c588255…
|
ragelink
|
643 |
entry["timestamp"] = _isoformat(entry.get("timestamp")) |
|
c588255…
|
ragelink
|
644 |
|
|
c588255…
|
ragelink
|
645 |
return {"filepath": args["filepath"], "history": history, "total": len(history)} |
|
c588255…
|
ragelink
|
646 |
|
|
c588255…
|
ragelink
|
647 |
|
|
c588255…
|
ragelink
|
648 |
# --------------------------------------------------------------------------- |
|
c588255…
|
ragelink
|
649 |
# SQL query handler |
|
c588255…
|
ragelink
|
650 |
# --------------------------------------------------------------------------- |
|
c588255…
|
ragelink
|
651 |
|
|
c588255…
|
ragelink
|
652 |
|
|
c588255…
|
ragelink
|
653 |
def _sql_query(args): |
|
c588255…
|
ragelink
|
654 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
655 |
from fossil.ticket_reports import TicketReport |
|
c588255…
|
ragelink
|
656 |
|
|
c588255…
|
ragelink
|
657 |
sql = args["sql"] |
|
c588255…
|
ragelink
|
658 |
error = TicketReport.validate_sql(sql) |
|
c588255…
|
ragelink
|
659 |
if error: |
|
c588255…
|
ragelink
|
660 |
return {"error": error} |
|
c588255…
|
ragelink
|
661 |
|
|
c588255…
|
ragelink
|
662 |
project, repo = _get_repo(args["slug"]) |
|
c588255…
|
ragelink
|
663 |
|
|
c588255…
|
ragelink
|
664 |
with FossilReader(repo.full_path) as reader: |
|
c588255…
|
ragelink
|
665 |
cursor = reader.conn.cursor() |
|
c588255…
|
ragelink
|
666 |
cursor.execute(sql) |
|
c588255…
|
ragelink
|
667 |
columns = [desc[0] for desc in cursor.description] if cursor.description else [] |
|
c588255…
|
ragelink
|
668 |
rows = cursor.fetchmany(500) |
|
c588255…
|
ragelink
|
669 |
return { |
|
c588255…
|
ragelink
|
670 |
"columns": columns, |
|
c588255…
|
ragelink
|
671 |
"rows": [list(row) for row in rows], |
|
c588255…
|
ragelink
|
672 |
"count": len(rows), |
|
c588255…
|
ragelink
|
673 |
} |