6570715c5cd53f3c7a71460a6806e13ade21909d
lrnassar
  Mon Mar 16 09:07:31 2026 -0700
Removing redmineDump script as it was replaced by redmineCli, refs conversation with Max.

diff --git src/utils/redmineDump src/utils/redmineDump
deleted file mode 100755
index bac187d381d..00000000000
--- src/utils/redmineDump
+++ /dev/null
@@ -1,247 +0,0 @@
-#!/usr/bin/env python3
-"""Dump a Redmine ticket discussion to stdout in Markdown format.
-
-Reads the Redmine API key from ~/.hg.conf (redmine.apiKey=...).
-
-Usage:
-    redmineDump 33571
-    redmineDump --images 33571
-    redmineDump --url https://redmine.gi.ucsc.edu/issues/33571
-"""
-
-import argparse
-import json
-import os
-import re
-import sys
-import tempfile
-import urllib.request
-from datetime import datetime
-
-DEFAULT_REDMINE = "https://redmine.gi.ucsc.edu"
-
-
-def read_api_key(conf_path="~/.hg.conf"):
-    """Read redmine.apiKey from ~/.hg.conf."""
-    conf_path = os.path.expanduser(conf_path)
-    with open(conf_path) as f:
-        for line in f:
-            line = line.strip()
-            if line.startswith("redmine.apiKey="):
-                return line.split("=", 1)[1]
-    sys.exit("Error: redmine.apiKey not found in " + conf_path)
-
-
-def api_get(base_url, path, api_key):
-    """GET a JSON endpoint from Redmine."""
-    url = base_url.rstrip("/") + path
-    req = urllib.request.Request(url)
-    req.add_header("X-Redmine-API-Key", api_key)
-    req.add_header("Accept", "application/json")
-    with urllib.request.urlopen(req) as resp:
-        return json.loads(resp.read())
-
-
-def download_file(url, dest_path, api_key):
-    """Download a file with API key auth."""
-    req = urllib.request.Request(url)
-    req.add_header("X-Redmine-API-Key", api_key)
-    with urllib.request.urlopen(req) as resp:
-        with open(dest_path, "wb") as f:
-            f.write(resp.read())
-
-
-def format_date(iso_str):
-    """Format an ISO date string nicely."""
-    dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
-    return dt.strftime("%Y-%m-%d %H:%M UTC")
-
-
-def redmine_textile_to_md(text):
-    """Convert common Redmine textile/wiki markup to Markdown."""
-    if not text:
-        return ""
-    # Bold: *text* -> **text** (but not bullet lists)
-    text = re.sub(r'(?<!\w)\*(\S.*?\S)\*(?!\w)', r'**\1**', text)
-    # Italic: _text_ -> *text*
-    text = re.sub(r'(?<!\w)_(\S.*?\S)_(?!\w)', r'*\1*', text)
-    # Inline code: @text@ -> `text`
-    text = re.sub(r'@([^@\n]+)@', r'`\1`', text)
-    # Code blocks: <pre>...</pre> -> ```...```
-    text = re.sub(r'<pre>\s*', '\n```\n', text)
-    text = re.sub(r'\s*</pre>', '\n```\n', text)
-    # Headings: h1. -> #, h2. -> ##, etc.
-    for i in range(1, 7):
-        text = re.sub(rf'^h{i}\.\s*', '#' * i + ' ', text, flags=re.MULTILINE)
-    # Redmine image references: !image.png! or !filename!
-    text = re.sub(r'!([^!\n]+\.(png|jpg|jpeg|gif))!', r'![image](\1)', text, flags=re.IGNORECASE)
-    # Links: "text":url -> [text](url)
-    text = re.sub(r'"([^"]+)":(\S+)', r'[\1](\2)', text)
-    # Issue references: #1234 -> link (leave as-is, just ensure they're visible)
-    return text
-
-
-def format_details(details):
-    """Format journal detail changes (status changes, assignments, etc.)."""
-    lines = []
-    for d in details:
-        prop = d.get("property", "")
-        name = d.get("name", "")
-        old = d.get("old_value", "")
-        new = d.get("new_value", "")
-        if prop == "attr":
-            if name == "status_id":
-                lines.append(f"  - Status changed: {old} -> {new}")
-            elif name == "assigned_to_id":
-                lines.append(f"  - Assignee changed: {old} -> {new}")
-            elif name == "done_ratio":
-                lines.append(f"  - Progress: {old}% -> {new}%")
-            else:
-                lines.append(f"  - {name}: {old} -> {new}")
-        elif prop == "attachment":
-            lines.append(f"  - Attached: {new}")
-    return "\n".join(lines)
-
-
-def main():
-    parser = argparse.ArgumentParser(description="Dump a Redmine ticket to Markdown")
-    parser.add_argument("ticket", nargs="?", help="Ticket ID number")
-    parser.add_argument("--url", help="Full Redmine issue URL (extracts ID and base URL)")
-    parser.add_argument("--redmine", default=DEFAULT_REDMINE, help="Redmine base URL (default: %(default)s)")
-    parser.add_argument("--images", action="store_true", help="Download images to a temp directory")
-    parser.add_argument("--conf", default="~/.hg.conf", help="Path to config file with redmine.apiKey")
-    args = parser.parse_args()
-
-    # Parse URL if given
-    if args.url:
-        m = re.match(r'(https?://[^/]+)/issues/(\d+)', args.url)
-        if not m:
-            sys.exit("Error: cannot parse URL: " + args.url)
-        args.redmine = m.group(1)
-        args.ticket = m.group(2)
-
-    if not args.ticket:
-        parser.print_help()
-        sys.exit(1)
-
-    ticket_id = str(args.ticket)
-    api_key = read_api_key(args.conf)
-    base_url = args.redmine
-
-    # Fetch ticket with journals and attachments
-    data = api_get(base_url, f"/issues/{ticket_id}.json?include=journals,attachments", api_key)
-    issue = data["issue"]
-
-    # Build attachment lookup: id -> attachment info
-    attachments = {a["id"]: a for a in issue.get("attachments", [])}
-    # Also build filename -> URL lookup for inline image references
-    attach_by_name = {a["filename"]: a for a in issue.get("attachments", [])}
-
-    # Setup image download directory
-    img_dir = None
-    if args.images:
-        img_dir = tempfile.mkdtemp(prefix=f"redmine_{ticket_id}_")
-        print(f"<!-- Images downloaded to: {img_dir} -->", file=sys.stderr)
-
-    def resolve_images(text):
-        """Replace image filenames with local paths if --images, or full URLs."""
-        if not text:
-            return text
-        def replace_img(m):
-            fname = m.group(1)
-            ext = m.group(2)
-            if fname in attach_by_name:
-                a = attach_by_name[fname]
-                if img_dir:
-                    local = os.path.join(img_dir, fname)
-                    if not os.path.exists(local):
-                        download_file(a["content_url"], local, api_key)
-                        print(f"  Downloaded: {local}", file=sys.stderr)
-                    return f"![image]({local})"
-                else:
-                    return f"![image]({a['content_url']})"
-            return m.group(0)
-        return re.sub(r'!\[image\]\(([^)]+\.(png|jpg|jpeg|gif))\)', replace_img, text, flags=re.IGNORECASE)
-
-    # Print header
-    out = []
-    out.append(f"# #{issue['id']}: {issue['subject']}")
-    out.append("")
-    out.append(f"- **Project:** {issue['project']['name']}")
-    out.append(f"- **Tracker:** {issue['tracker']['name']}")
-    out.append(f"- **Status:** {issue['status']['name']}")
-    out.append(f"- **Priority:** {issue['priority']['name']}")
-    out.append(f"- **Author:** {issue['author']['name']}")
-    if issue.get("assigned_to"):
-        out.append(f"- **Assigned to:** {issue['assigned_to']['name']}")
-    out.append(f"- **Created:** {format_date(issue['created_on'])}")
-    out.append(f"- **Updated:** {format_date(issue['updated_on'])}")
-    if issue.get("closed_on"):
-        out.append(f"- **Closed:** {format_date(issue['closed_on'])}")
-    out.append(f"- **URL:** {base_url}/issues/{ticket_id}")
-    out.append("")
-
-    # Attachments summary
-    if attachments:
-        out.append("## Attachments")
-        out.append("")
-        for a in issue["attachments"]:
-            out.append(f"- [{a['filename']}]({a['content_url']}) ({a['filesize']} bytes, {a['author']['name']}, {format_date(a['created_on'])})")
-        out.append("")
-
-    # Description
-    out.append("## Description")
-    out.append("")
-    desc = redmine_textile_to_md(issue.get("description", ""))
-    desc = resolve_images(desc)
-    out.append(desc)
-    out.append("")
-
-    # Journals (discussion)
-    journals = issue.get("journals", [])
-    if journals:
-        out.append("---")
-        out.append("## Discussion")
-        out.append("")
-
-    for j in journals:
-        notes = j.get("notes", "")
-        details = j.get("details", [])
-        # Skip empty journals (no notes and no interesting details)
-        if not notes and not details:
-            continue
-
-        user = j["user"]["name"]
-        date = format_date(j["created_on"])
-        out.append(f"### {user} — {date}")
-        out.append("")
-
-        if details:
-            detail_text = format_details(details)
-            if detail_text:
-                out.append(detail_text)
-                out.append("")
-
-        if notes:
-            md_notes = redmine_textile_to_md(notes)
-            md_notes = resolve_images(md_notes)
-            out.append(md_notes)
-            out.append("")
-
-        out.append("---")
-        out.append("")
-
-    # Download any remaining images not referenced inline
-    if img_dir:
-        for a in issue["attachments"]:
-            if a["content_type"] and a["content_type"].startswith("image/"):
-                local = os.path.join(img_dir, a["filename"])
-                if not os.path.exists(local):
-                    download_file(a["content_url"], local, api_key)
-                    print(f"  Downloaded: {local}", file=sys.stderr)
-
-    print("\n".join(out))
-
-
-if __name__ == "__main__":
-    main()