ec013b7cd85204640ee339dd860b6c819c912bf0 lrnassar Mon Mar 16 09:31:08 2026 -0700 More feedback from CR. diff --git src/utils/redmineCli src/utils/redmineCli index 986f4d5ff5a..fd724fc43ab 100755 --- src/utils/redmineCli +++ src/utils/redmineCli @@ -217,44 +217,44 @@ # --------------------------------------------------------------------------- # Subcommand: show # --------------------------------------------------------------------------- def cmd_show(args): """Display a single ticket in Markdown.""" data = api_get(args.base_url, f"/issues/{args.ticket_id}.json?include=journals,attachments", args.api_key) issue = data["issue"] attachments = {a["id"]: a for a in issue.get("attachments", [])} attach_by_name = {a["filename"]: a for a in issue.get("attachments", [])} - img_dir = None - if args.images: - img_dir = tempfile.mkdtemp(prefix=f"redmine_{args.ticket_id}_") - print(f"<!-- Images downloaded to: {img_dir} -->", file=sys.stderr) + dl_dir = None + if args.images or args.download_all: + dl_dir = tempfile.mkdtemp(prefix=f"redmine_{args.ticket_id}_") + print(f"<!-- Attachments downloaded to: {dl_dir} -->", file=sys.stderr) def resolve_images(text): if not text: return text def replace_img(m): fname = m.group(1) if fname in attach_by_name: a = attach_by_name[fname] - if img_dir: - local = os.path.join(img_dir, fname) + if dl_dir: + local = os.path.join(dl_dir, fname) if not os.path.exists(local): download_file(a["content_url"], local, args.api_key) print(f" Downloaded: {local}", file=sys.stderr) return f"" else: return f"" return m.group(0) return re.sub(r'!\[image\]\(([^)]+\.(png|jpg|jpeg|gif))\)', replace_img, text, flags=re.IGNORECASE) out = [] out.append(f"# #{issue['id']}: {issue['subject']}") out.append("") out.append(f"- **Project:** {issue['project']['name']}") out.append(f"- **Tracker:** {issue['tracker']['name']}") @@ -307,34 +307,35 @@ 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("") - if img_dir: + if dl_dir: for a in issue["attachments"]: - if a.get("content_type", "").startswith("image/"): - local = os.path.join(img_dir, a["filename"]) + is_image = a.get("content_type", "").startswith("image/") + if args.download_all or is_image: + local = os.path.join(dl_dir, a["filename"]) if not os.path.exists(local): download_file(a["content_url"], local, args.api_key) print(f" Downloaded: {local}", file=sys.stderr) print("\n".join(out)) 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, timeout=30) as resp: with open(dest_path, "wb") as f: f.write(resp.read()) @@ -533,34 +534,36 @@ # Argument parsing # --------------------------------------------------------------------------- def build_parser(): parser = argparse.ArgumentParser( prog="redmineCli", description="Redmine CLI for the UCSC Genome Browser team") parser.add_argument("--redmine", default=DEFAULT_REDMINE, help="Redmine base URL (default: %(default)s)") parser.add_argument("--conf", default="~/.hg.conf", help="Config file with redmine.apiKey (default: %(default)s)") sub = parser.add_subparsers(dest="command", required=True) # show - p_show = sub.add_parser("show", help="Display a ticket in Markdown") + p_show = sub.add_parser("show", help="Display a ticket in Markdown, optionally download attachments") p_show.add_argument("ticket_id", help="Ticket ID number") p_show.add_argument("--images", action="store_true", help="Download images to a temp directory") + p_show.add_argument("--download-all", dest="download_all", action="store_true", + help="Download all attachments to a temp directory") # list p_list = sub.add_parser("list", help="List/search tickets") p_list.add_argument("--project", default=DEFAULT_PROJECT, help="Project identifier (default: %(default)s)") p_list.add_argument("--status", default="open", help="Status filter: open, closed, * (default: %(default)s)") p_list.add_argument("--assigned-to", dest="assigned_to", help="Assignee name or 'me'") p_list.add_argument("--tracker", help="Tracker name or ID") p_list.add_argument("--search", help="Search in subject") p_list.add_argument("--limit", type=int, default=25, help="Max results (default: %(default)s)")