From 98927a0ae049e10d8fe4e23c456534b9f330a664 Mon Sep 17 00:00:00 2001 From: Calin Marian Date: Mon, 16 Mar 2026 14:33:45 +0200 Subject: [PATCH 1/4] Improve text formatting with ADF rendering and HTML link preservation Rewrite HTML tag stripper to preserve links (as "text (url)") and images (as "[image: url]") instead of stripping all tags. Add Atlassian Document Format (ADF) renderer for Jira Cloud descriptions, handling text with link marks, inlineCard/embedCard smart links, mentions, hard breaks, and media nodes. Fix formatJiraIssue to handle ADF object descriptions in addition to plain string descriptions. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/formatter.zig | 293 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 256 insertions(+), 37 deletions(-) diff --git a/src/formatter.zig b/src/formatter.zig index f91c664..d52c2c4 100644 --- a/src/formatter.zig +++ b/src/formatter.zig @@ -5,57 +5,128 @@ pub const OutputFormat = enum { json, }; -/// Simple HTML tag stripper - removes HTML tags and decodes basic entities +/// Extract the value of an HTML attribute from the inner content of a tag. +/// E.g., given `a href="https://example.com" class="link"` and "href", +/// returns "https://example.com" (a slice into tag_inner). +fn extractHtmlAttr(tag_inner: []const u8, attr_name: []const u8) ?[]const u8 { + var pos: usize = 0; + while (pos + attr_name.len < tag_inner.len) : (pos += 1) { + if (!std.mem.startsWith(u8, tag_inner[pos..], attr_name)) continue; + // Must be preceded by whitespace to avoid partial matches (e.g. "data-href") + if (pos > 0 and tag_inner[pos - 1] != ' ' and tag_inner[pos - 1] != '\t' and tag_inner[pos - 1] != '\n') continue; + var p = pos + attr_name.len; + // Skip optional whitespace around = + while (p < tag_inner.len and (tag_inner[p] == ' ' or tag_inner[p] == '\t')) : (p += 1) {} + if (p >= tag_inner.len or tag_inner[p] != '=') continue; + p += 1; + while (p < tag_inner.len and (tag_inner[p] == ' ' or tag_inner[p] == '\t')) : (p += 1) {} + if (p >= tag_inner.len) continue; + const quote = tag_inner[p]; + if (quote != '"' and quote != '\'') continue; + const val_start = p + 1; + const val_end = std.mem.indexOfScalarPos(u8, tag_inner, val_start, quote) orelse continue; + return tag_inner[val_start..val_end]; + } + return null; +} + +/// HTML tag stripper that preserves links and images. +/// text → text (url) [or just text if text == url] +/// → [image: url] fn stripHtmlTags(allocator: std.mem.Allocator, html: []const u8) ![]u8 { var result = std.ArrayList(u8).initCapacity(allocator, html.len) catch return try allocator.dupe(u8, html); errdefer result.deinit(allocator); - var in_tag = false; + var i: usize = 0; var in_entity = false; var entity_buffer: [10]u8 = undefined; var entity_len: usize = 0; - var i: usize = 0; - while (i < html.len) : (i += 1) { + // Track current href to append URL after link text if it differs + var link_href: ?[]const u8 = null; + var link_text_start: usize = 0; + + while (i < html.len) { const c = html[i]; if (c == '<') { - in_tag = true; - } else if (c == '>') { - in_tag = false; - // Add space after closing tag for readability - if (i + 1 < html.len and html[i + 1] != ' ' and html[i + 1] != '\n') { - result.append(allocator, ' ') catch {}; - } - } else if (!in_tag) { - if (c == '&') { - in_entity = true; - entity_len = 0; - } else if (in_entity) { - if (c == ';') { - in_entity = false; - const entity = entity_buffer[0..entity_len]; - if (std.mem.eql(u8, entity, "nbsp")) { - result.append(allocator, ' ') catch {}; - } else if (std.mem.eql(u8, entity, "lt")) { - result.append(allocator, '<') catch {}; - } else if (std.mem.eql(u8, entity, "gt")) { - result.append(allocator, '>') catch {}; - } else if (std.mem.eql(u8, entity, "amp")) { - result.append(allocator, '&') catch {}; - } else if (std.mem.eql(u8, entity, "quot")) { - result.append(allocator, '"') catch {}; - } else { - // Unknown entity, just skip + const close = std.mem.indexOfScalarPos(u8, html, i + 1, '>') orelse { + i += 1; + continue; + }; + const tag_inner = html[i + 1 .. close]; + const trimmed = std.mem.trimLeft(u8, tag_inner, " \t\n\r"); + + if (trimmed.len > 0 and trimmed[0] == 'a' and + (trimmed.len == 1 or trimmed[1] == ' ' or trimmed[1] == '\t' or trimmed[1] == '\n')) + { + // Opening tag - extract href + link_href = extractHtmlAttr(tag_inner, "href"); + link_text_start = result.items.len; + } else if (trimmed.len >= 2 and trimmed[0] == '/' and trimmed[1] == 'a' and + (trimmed.len == 2 or trimmed[2] == ' ' or trimmed[2] == '\t')) + { + // Closing tag - append URL if link text differs from href + if (link_href) |href| { + const link_text = result.items[link_text_start..result.items.len]; + const trimmed_text = std.mem.trim(u8, link_text, " \t\n\r"); + if (!std.mem.eql(u8, trimmed_text, href)) { + try result.appendSlice(allocator, " ("); + try result.appendSlice(allocator, href); + try result.appendSlice(allocator, ")"); } - } else if (entity_len < entity_buffer.len) { - entity_buffer[entity_len] = c; - entity_len += 1; + link_href = null; + } + } else if (trimmed.len >= 3 and std.mem.startsWith(u8, trimmed, "img") and + (trimmed.len == 3 or trimmed[3] == ' ' or trimmed[3] == '/' or trimmed[3] == '>')) + { + // tag - show src URL + if (extractHtmlAttr(tag_inner, "src")) |src| { + try result.appendSlice(allocator, "[image: "); + try result.appendSlice(allocator, src); + try result.appendSlice(allocator, "]"); } } else { - result.append(allocator, c) catch {}; + // Other tag - add space after for readability + if (close + 1 < html.len and html[close + 1] != ' ' and html[close + 1] != '\n') { + result.append(allocator, ' ') catch {}; + } + } + + i = close + 1; + continue; + } + + // Entity handling + if (c == '&') { + in_entity = true; + entity_len = 0; + } else if (in_entity) { + if (c == ';') { + in_entity = false; + const entity = entity_buffer[0..entity_len]; + if (std.mem.eql(u8, entity, "nbsp")) { + result.append(allocator, ' ') catch {}; + } else if (std.mem.eql(u8, entity, "lt")) { + result.append(allocator, '<') catch {}; + } else if (std.mem.eql(u8, entity, "gt")) { + result.append(allocator, '>') catch {}; + } else if (std.mem.eql(u8, entity, "amp")) { + result.append(allocator, '&') catch {}; + } else if (std.mem.eql(u8, entity, "quot")) { + result.append(allocator, '"') catch {}; + } else { + // Unknown entity, skip + } + } else if (entity_len < entity_buffer.len) { + entity_buffer[entity_len] = c; + entity_len += 1; } + } else { + result.append(allocator, c) catch {}; } + + i += 1; } return result.toOwnedSlice(allocator); @@ -82,6 +153,149 @@ fn cleanWhitespace(allocator: std.mem.Allocator, text: []const u8) ![]u8 { return result.toOwnedSlice(allocator); } +/// Write plain text extracted from a Jira ADF (Atlassian Document Format) node. +/// Iterative DFS to avoid per-recursion allocations. +fn writeAdfText(writer: anytype, node: std.json.Value) !void { + const max_stack = 64; + var stack: [max_stack]struct { items: []const std.json.Value, idx: usize } = undefined; + var depth: usize = 0; + + // Seed with the root node's content array + if (node != .object) return; + const root_content = node.object.get("content") orelse return; + if (root_content != .array) return; + stack[0] = .{ .items = root_content.array.items, .idx = 0 }; + depth = 1; + + while (depth > 0) { + const frame = &stack[depth - 1]; + if (frame.idx >= frame.items.len) { + depth -= 1; + continue; + } + const child = frame.items[frame.idx]; + frame.idx += 1; + + if (child != .object) continue; + const obj = child.object; + + // Emit text content + if (obj.get("text")) |text_val| { + if (text_val == .string) { + try writer.writeAll(text_val.string); + // Check for link marks and append URL if it differs from text + if (obj.get("marks")) |marks_val| { + if (marks_val == .array) { + for (marks_val.array.items) |mark| { + if (mark != .object) continue; + const mt = if (mark.object.get("type")) |t| if (t == .string) t.string else null else null; + if (mt) |mark_type| { + if (std.mem.eql(u8, mark_type, "link")) { + if (mark.object.get("attrs")) |attrs| { + if (attrs == .object) { + if (attrs.object.get("href")) |href| { + if (href == .string and !std.mem.eql(u8, text_val.string, href.string)) { + try writer.writeAll(" ("); + try writer.writeAll(href.string); + try writer.writeAll(")"); + } + } + } + } + } + } + } + } + } + } + } + + // Handle inlineCard (smart links / embedded URLs) + { + const nt = if (obj.get("type")) |t| if (t == .string) t.string else null else null; + if (nt) |node_type| { + if (std.mem.eql(u8, node_type, "inlineCard") or std.mem.eql(u8, node_type, "embedCard")) { + if (obj.get("attrs")) |attrs| { + if (attrs == .object) { + if (attrs.object.get("url")) |url| { + if (url == .string) { + try writer.writeAll(url.string); + try writer.writeAll("\n"); + } + } + } + } + } else if (std.mem.eql(u8, node_type, "mention")) { + if (obj.get("attrs")) |attrs| { + if (attrs == .object) { + if (attrs.object.get("text")) |text| { + if (text == .string) { + try writer.writeAll(text.string); + } + } + } + } + } else if (std.mem.eql(u8, node_type, "hardBreak")) { + try writer.writeAll("\n"); + } else if (std.mem.eql(u8, node_type, "media")) { + if (obj.get("attrs")) |attrs| { + if (attrs == .object) { + const mt = if (attrs.object.get("type")) |t| if (t == .string) t.string else null else null; + if (mt) |media_type| { + if (std.mem.eql(u8, media_type, "external")) { + if (attrs.object.get("url")) |url| { + if (url == .string) { + try writer.writeAll("[image: "); + try writer.writeAll(url.string); + try writer.writeAll("]"); + } + } + } else { + const alt = if (attrs.object.get("alt")) |a| if (a == .string) a.string else null else null; + if (alt) |alt_text| { + try writer.writeAll("[image: "); + try writer.writeAll(alt_text); + try writer.writeAll("]"); + } else { + try writer.writeAll("[image]"); + } + } + } + } + } + } + } + } + + // Push child's content array onto stack + if (obj.get("content")) |content_val| { + if (content_val == .array and depth < max_stack) { + stack[depth] = .{ .items = content_val.array.items, .idx = 0 }; + depth += 1; + continue; // process children before emitting newline + } + } + + // Newline after block-level nodes (only when no children were pushed) + if (obj.get("type")) |type_val| { + if (type_val == .string) { + const t = type_val.string; + if (std.mem.eql(u8, t, "paragraph") or + std.mem.eql(u8, t, "heading") or + std.mem.eql(u8, t, "bulletList") or + std.mem.eql(u8, t, "orderedList") or + std.mem.eql(u8, t, "listItem") or + std.mem.eql(u8, t, "codeBlock") or + std.mem.eql(u8, t, "blockquote") or + std.mem.eql(u8, t, "rule")) + { + try writer.writeAll("\n"); + } + } + } + } +} + /// Format Confluence search results as readable text pub fn formatConfluenceSearchResults(allocator: std.mem.Allocator, json_str: []const u8, show_full_content: bool) ![]u8 { var parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_str, .{}); @@ -336,12 +550,17 @@ pub fn formatJiraIssue(allocator: std.mem.Allocator, json_str: []const u8) ![]u8 try writer.print("Updated: {s}\n", .{updated.string}); } - // Description + // Description (can be a plain string or an ADF object in Jira Cloud) if (fields_obj.get("description")) |description| { if (description != .null) { try writer.writeAll("\nDescription:\n"); try writer.writeAll("─────────────────────────────────────────\n"); - try writer.print("{s}\n", .{description.string}); + if (description == .string) { + try writer.print("{s}\n", .{description.string}); + } else if (description == .object) { + try writeAdfText(writer, description); + try writer.writeAll("\n"); + } } } From 1e12dd1ba849b4128e35d0f2e0ef39075071ba32 Mon Sep 17 00:00:00 2001 From: Calin Marian Date: Mon, 16 Mar 2026 14:49:08 +0200 Subject: [PATCH 2/4] Address review feedback on formatter improvements Fix off-by-one in extractHtmlAttr loop bound and use std.ascii.isWhitespace for consistent whitespace checks. Propagate allocation errors in stripHtmlTags instead of silently swallowing them. Fix writeAdfText to emit newlines after block-level nodes by tracking is_block in the DFS stack, so paragraphs and headings are properly separated. Add [image] fallback for external media nodes missing a URL. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/formatter.zig | 61 ++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/src/formatter.zig b/src/formatter.zig index d52c2c4..96cbd40 100644 --- a/src/formatter.zig +++ b/src/formatter.zig @@ -10,16 +10,16 @@ pub const OutputFormat = enum { /// returns "https://example.com" (a slice into tag_inner). fn extractHtmlAttr(tag_inner: []const u8, attr_name: []const u8) ?[]const u8 { var pos: usize = 0; - while (pos + attr_name.len < tag_inner.len) : (pos += 1) { + while (pos + attr_name.len <= tag_inner.len) : (pos += 1) { if (!std.mem.startsWith(u8, tag_inner[pos..], attr_name)) continue; // Must be preceded by whitespace to avoid partial matches (e.g. "data-href") - if (pos > 0 and tag_inner[pos - 1] != ' ' and tag_inner[pos - 1] != '\t' and tag_inner[pos - 1] != '\n') continue; + if (pos > 0 and !std.ascii.isWhitespace(tag_inner[pos - 1])) continue; var p = pos + attr_name.len; // Skip optional whitespace around = - while (p < tag_inner.len and (tag_inner[p] == ' ' or tag_inner[p] == '\t')) : (p += 1) {} + while (p < tag_inner.len and std.ascii.isWhitespace(tag_inner[p])) : (p += 1) {} if (p >= tag_inner.len or tag_inner[p] != '=') continue; p += 1; - while (p < tag_inner.len and (tag_inner[p] == ' ' or tag_inner[p] == '\t')) : (p += 1) {} + while (p < tag_inner.len and std.ascii.isWhitespace(tag_inner[p])) : (p += 1) {} if (p >= tag_inner.len) continue; const quote = tag_inner[p]; if (quote != '"' and quote != '\'') continue; @@ -89,7 +89,7 @@ fn stripHtmlTags(allocator: std.mem.Allocator, html: []const u8) ![]u8 { } else { // Other tag - add space after for readability if (close + 1 < html.len and html[close + 1] != ' ' and html[close + 1] != '\n') { - result.append(allocator, ' ') catch {}; + try result.append(allocator, ' '); } } @@ -106,15 +106,15 @@ fn stripHtmlTags(allocator: std.mem.Allocator, html: []const u8) ![]u8 { in_entity = false; const entity = entity_buffer[0..entity_len]; if (std.mem.eql(u8, entity, "nbsp")) { - result.append(allocator, ' ') catch {}; + try result.append(allocator, ' '); } else if (std.mem.eql(u8, entity, "lt")) { - result.append(allocator, '<') catch {}; + try result.append(allocator, '<'); } else if (std.mem.eql(u8, entity, "gt")) { - result.append(allocator, '>') catch {}; + try result.append(allocator, '>'); } else if (std.mem.eql(u8, entity, "amp")) { - result.append(allocator, '&') catch {}; + try result.append(allocator, '&'); } else if (std.mem.eql(u8, entity, "quot")) { - result.append(allocator, '"') catch {}; + try result.append(allocator, '"'); } else { // Unknown entity, skip } @@ -123,7 +123,7 @@ fn stripHtmlTags(allocator: std.mem.Allocator, html: []const u8) ![]u8 { entity_len += 1; } } else { - result.append(allocator, c) catch {}; + try result.append(allocator, c); } i += 1; @@ -157,19 +157,22 @@ fn cleanWhitespace(allocator: std.mem.Allocator, text: []const u8) ![]u8 { /// Iterative DFS to avoid per-recursion allocations. fn writeAdfText(writer: anytype, node: std.json.Value) !void { const max_stack = 64; - var stack: [max_stack]struct { items: []const std.json.Value, idx: usize } = undefined; + var stack: [max_stack]struct { items: []const std.json.Value, idx: usize, is_block: bool } = undefined; var depth: usize = 0; // Seed with the root node's content array if (node != .object) return; const root_content = node.object.get("content") orelse return; if (root_content != .array) return; - stack[0] = .{ .items = root_content.array.items, .idx = 0 }; + stack[0] = .{ .items = root_content.array.items, .idx = 0, .is_block = false }; depth = 1; while (depth > 0) { const frame = &stack[depth - 1]; if (frame.idx >= frame.items.len) { + if (frame.is_block) { + try writer.writeAll("\n"); + } depth -= 1; continue; } @@ -248,7 +251,11 @@ fn writeAdfText(writer: anytype, node: std.json.Value) !void { try writer.writeAll("[image: "); try writer.writeAll(url.string); try writer.writeAll("]"); + } else { + try writer.writeAll("[image]"); } + } else { + try writer.writeAll("[image]"); } } else { const alt = if (attrs.object.get("alt")) |a| if (a == .string) a.string else null else null; @@ -270,25 +277,29 @@ fn writeAdfText(writer: anytype, node: std.json.Value) !void { // Push child's content array onto stack if (obj.get("content")) |content_val| { if (content_val == .array and depth < max_stack) { - stack[depth] = .{ .items = content_val.array.items, .idx = 0 }; + // Track whether this is a block-level node so we emit a newline after its children + const is_block = if (obj.get("type")) |tv| if (tv == .string) blk: { + const t = tv.string; + break :blk (std.mem.eql(u8, t, "paragraph") or + std.mem.eql(u8, t, "heading") or + std.mem.eql(u8, t, "bulletList") or + std.mem.eql(u8, t, "orderedList") or + std.mem.eql(u8, t, "listItem") or + std.mem.eql(u8, t, "codeBlock") or + std.mem.eql(u8, t, "blockquote") or + std.mem.eql(u8, t, "rule")); + } else false else false; + stack[depth] = .{ .items = content_val.array.items, .idx = 0, .is_block = is_block }; depth += 1; - continue; // process children before emitting newline + continue; } } - // Newline after block-level nodes (only when no children were pushed) + // Newline after leaf block-level nodes (no content array) if (obj.get("type")) |type_val| { if (type_val == .string) { const t = type_val.string; - if (std.mem.eql(u8, t, "paragraph") or - std.mem.eql(u8, t, "heading") or - std.mem.eql(u8, t, "bulletList") or - std.mem.eql(u8, t, "orderedList") or - std.mem.eql(u8, t, "listItem") or - std.mem.eql(u8, t, "codeBlock") or - std.mem.eql(u8, t, "blockquote") or - std.mem.eql(u8, t, "rule")) - { + if (std.mem.eql(u8, t, "rule")) { try writer.writeAll("\n"); } } From baff6b4f54ba7258b8b1d1e05a0703d98810ee2c Mon Sep 17 00:00:00 2001 From: Calin Marian Date: Mon, 16 Mar 2026 14:35:59 +0200 Subject: [PATCH 3/4] Add jira comments subcommand Add `jira comments ` command to display issue comments with formatted ADF body text. Text output shows timestamp, author, and comment body. JSON output passes through the raw API response. Also include the comment field in `jira issue --format=json` output for programmatic use. --- src/formatter.zig | 60 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 22 ++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/formatter.zig b/src/formatter.zig index 96cbd40..41e7ef0 100644 --- a/src/formatter.zig +++ b/src/formatter.zig @@ -714,3 +714,63 @@ pub fn formatGenericList(allocator: std.mem.Allocator, json_str: []const u8, ite return try allocator.dupe(u8, "No items found.\n"); } + +/// Format Jira issue comments as readable text +pub fn formatJiraComments(allocator: std.mem.Allocator, json_str: []const u8, issue_key: []const u8) ![]u8 { + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_str, .{}); + defer parsed.deinit(); + + const root = parsed.value.object; + const comments = root.get("comments") orelse return try allocator.dupe(u8, "No comments found.\n"); + const comments_array = comments.array; + + var output = try std.ArrayList(u8).initCapacity(allocator, 1024); + errdefer output.deinit(allocator); + const writer = output.writer(allocator); + + try writer.print("Comments for {s} ({} comment{s})\n", .{ + issue_key, + comments_array.items.len, + if (comments_array.items.len == 1) @as([]const u8, "") else "s", + }); + try writer.writeAll("─────────────────────────────────────────\n"); + + for (comments_array.items) |comment| { + const obj = comment.object; + + // Date — extract "YYYY-MM-DD HH:MM" from ISO 8601 + if (obj.get("created")) |created| { + if (created == .string) { + const s = created.string; + if (s.len >= 16) { + try writer.print("{s} {s}", .{ s[0..10], s[11..16] }); + } else { + try writer.print("{s}", .{s}); + } + } + } + + // Author + if (obj.get("author")) |author| { + if (author == .object) { + if (author.object.get("displayName")) |name| { + if (name == .string) { + try writer.print(" \u{2014} {s}", .{name.string}); + } + } + } + } + try writer.writeAll(":\n"); + + // Body (ADF) + if (obj.get("body")) |body| { + if (body == .object) { + try writeAdfText(writer, body); + } + } + + try writer.writeAll("\n"); + } + + return output.toOwnedSlice(allocator); +} diff --git a/src/main.zig b/src/main.zig index bf84628..6955a4b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -22,6 +22,7 @@ const JiraCommand = enum { boards, sprints, @"sprint-issues", + comments, user, help, }; @@ -69,6 +70,7 @@ fn printHelp() !void { \\ boards [--type=scrum] List agile boards \\ sprints [--state=active] List sprints \\ sprint-issues [--max=50] Get issues in sprint + \\ comments Get issue comments \\ user Get current user info \\ \\Confluence Commands: @@ -106,6 +108,7 @@ fn printJiraHelp() !void { \\ boards [--type=scrum] List agile boards \\ sprints [--state=active] List sprints \\ sprint-issues [--max=50] Get issues in sprint + \\ comments Get issue comments \\ user Get current user info \\ \\JQL Examples: @@ -189,7 +192,8 @@ fn handleJiraCommand(allocator: std.mem.Allocator, client: *AtlassianClient, arg std.debug.print("Usage: jira issue [--format=text|json]\n", .{}); return; } - const response = try jira.getIssue(args[1], null); + const issue_fields: ?[]const u8 = if (output_format == .json) "summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype,comment" else null; + const response = try jira.getIssue(args[1], issue_fields); defer allocator.free(response); if (output_format == .text) { @@ -292,6 +296,22 @@ fn handleJiraCommand(allocator: std.mem.Allocator, client: *AtlassianClient, arg defer allocator.free(response); std.debug.print("{s}\n", .{response}); }, + .comments => { + if (args.len < 2) { + std.debug.print("Usage: jira comments [--format=text|json]\n", .{}); + return; + } + const response = try jira.getComments(args[1]); + defer allocator.free(response); + + if (output_format == .text) { + const formatted = try formatter.formatJiraComments(allocator, response, args[1]); + defer allocator.free(formatted); + std.debug.print("{s}", .{formatted}); + } else { + std.debug.print("{s}\n", .{response}); + } + }, .user => { const response = try jira.getCurrentUser(); defer allocator.free(response); From bd61129fe8ff13c25a80f21aef32f1eb33ddbc30 Mon Sep 17 00:00:00 2001 From: Calin Marian Date: Mon, 16 Mar 2026 14:50:11 +0200 Subject: [PATCH 4/4] Address review feedback for jira comments Extract JSON issue fields string to a named constant for readability. --- src/main.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 6955a4b..4d8102f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -192,7 +192,8 @@ fn handleJiraCommand(allocator: std.mem.Allocator, client: *AtlassianClient, arg std.debug.print("Usage: jira issue [--format=text|json]\n", .{}); return; } - const issue_fields: ?[]const u8 = if (output_format == .json) "summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype,comment" else null; + const json_issue_fields = "summary,description,status,assignee,reporter,labels,priority,created,updated,issuetype,comment"; + const issue_fields: ?[]const u8 = if (output_format == .json) json_issue_fields else null; const response = try jira.getIssue(args[1], issue_fields); defer allocator.free(response);