07c109ea4fe627879544dddf222650ab8c29eaf8 max Tue Apr 21 02:55:53 2026 -0700 redmineCli: add --file-list-add PATH option to append paths idempotently, refs #35059 Existing --file-list overwrites the custom field. The new --file-list-add reads the current value, appends each path that isn't already listed, and writes the combined value back with CRLF separators (matching Redmine's internal format). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> diff --git src/utils/redmineCli src/utils/redmineCli index 590f0ac50b0..5b6b24c76c2 100755 --- src/utils/redmineCli +++ src/utils/redmineCli @@ -648,55 +648,70 @@ args.target_version, project_id, args.base_url, args.api_key) custom_fields = [] if args.category is not None: custom_fields.append({"id": CF_CATEGORY, "value": args.category}) if args.mlm is not None: custom_fields.append({"id": CF_MLM, "value": args.mlm}) if args.release_log_text is not None: custom_fields.append({"id": CF_RELEASE_LOG_TEXT, "value": args.release_log_text}) if args.release_log_url is not None: custom_fields.append({"id": CF_RELEASE_LOG_URL, "value": args.release_log_url}) if args.released_to_rr is not None: custom_fields.append({"id": CF_RELEASED_TO_RR, "value": args.released_to_rr}) if args.file_list is not None: custom_fields.append({"id": CF_FILE_LIST, "value": args.file_list}) + if args.file_list_add: + existing = api_get(args.base_url, + f"/issues/{args.ticket_id}.json", + args.api_key)["issue"] + current = "" + for cf in existing.get("custom_fields", []): + if cf["id"] == CF_FILE_LIST: + current = cf.get("value") or "" + break + lines = [l for l in current.splitlines() if l.strip()] + for path in args.file_list_add: + if path not in lines: + lines.append(path) + custom_fields.append({"id": CF_FILE_LIST, + "value": "\r\n".join(lines)}) if args.table_list is not None: custom_fields.append({"id": CF_TABLE_LIST, "value": args.table_list}) if args.assemblies is not None: custom_fields.append({"id": CF_ASSEMBLIES, "value": args.assemblies}) for cf_spec in (args.custom_field or []): if "=" not in cf_spec: sys.exit(f"Error: --custom-field must be ID=VALUE, got: {cf_spec}") cf_id, cf_val = cf_spec.split("=", 1) if not cf_id.isdigit(): sys.exit(f"Error: custom field ID must be numeric, got: {cf_id}") custom_fields.append({"id": int(cf_id), "value": cf_val}) if custom_fields: issue_data["custom_fields"] = custom_fields note = read_text_input(args.note, args.note_file) if note: issue_data["notes"] = strip_emoji(prepend_attribution(note)) if not issue_data: sys.exit("Error: no fields to update. Provide at least one of: " "--status, --assigned-to, --priority, --subject, " "--target-version, --category, --mlm, " "--release-log-text, --release-log-url, " - "--released-to-rr, --file-list, --table-list, --assemblies, " - "--custom-field, --note") + "--released-to-rr, --file-list, --file-list-add, " + "--table-list, --assemblies, --custom-field, --note") data = {"issue": issue_data} api_put(args.base_url, f"/issues/{args.ticket_id}.json", args.api_key, data) print(f"Updated #{args.ticket_id}: {make_url(args.base_url, args.ticket_id)}") # --------------------------------------------------------------------------- # Subcommand: attach # --------------------------------------------------------------------------- def cmd_attach(args): """Upload an attachment to a ticket.""" filepath = args.file if not os.path.isfile(filepath): sys.exit(f"Error: file not found: {filepath}") @@ -957,31 +972,35 @@ p_update.add_argument("--assigned-to", dest="assigned_to", help="Assignee name/ID (empty string to clear)") p_update.add_argument("--priority", type=int, help="New priority ID") p_update.add_argument("--subject", help="New subject") p_update.add_argument("--target-version", dest="target_version", help="Target version name or ID (empty string to clear)") p_update.add_argument("--category", help="MLQ Category") p_update.add_argument("--mlm", help="MLM name") p_update.add_argument("--release-log-text", dest="release_log_text", help="Release Log Text (custom field)") p_update.add_argument("--release-log-url", dest="release_log_url", help="Release Log URL (custom field)") p_update.add_argument("--released-to-rr", dest="released_to_rr", help="Released to RR (custom field, 0 or 1)") p_update.add_argument("--file-list", dest="file_list", - help="File List (custom field)") + help="File List (custom field, overwrites existing value)") + p_update.add_argument("--file-list-add", dest="file_list_add", + action="append", metavar="PATH", + help="Append PATH to the File List custom field " + "(repeatable; idempotent, skips paths already present)") p_update.add_argument("--table-list", dest="table_list", help="Table List (custom field)") p_update.add_argument("--assemblies", help="Assemblies (custom field)") p_update.add_argument("--custom-field", dest="custom_field", action="append", metavar="ID=VALUE", help="Set arbitrary custom field by ID (repeatable)") p_update.add_argument("--note", help="Comment to include with update") p_update.add_argument("--note-file", dest="note_file", help="Read note from file (- for stdin)") # attach p_attach = sub.add_parser("attach", help="Upload an attachment") p_attach.add_argument("ticket_id", help="Ticket ID number") p_attach.add_argument("file", help="File path to upload")