FossilRepo

fossilrepo / mcp_server / tools.py
Source Blame History 673 lines
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 }

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button