Skip to content

Commit 910e502

Browse files
grokifyclaude
andcommitted
feat(cli): add patch, export, and fields subcommands
Add three new subcommands to the gojira CLI: - patch: Update issue fields (set values, add/remove labels) - export: Export issues to JSON or XLSX format - fields: List and filter custom fields These replace the ad-hoc development scripts in cmd/issue_query/, cmd/issues_query/, and cmd/savin/ with proper CLI interfaces. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d417a3d commit 910e502

3 files changed

Lines changed: 577 additions & 0 deletions

File tree

cmd/gojira/export.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/grokify/gojira/rest"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
var exportCmd = &cobra.Command{
15+
Use: "export [flags]",
16+
Short: "Export issues to JSON or XLSX",
17+
Long: `Export Jira issues to JSON or XLSX format.
18+
19+
Examples:
20+
# Export search results to JSON
21+
gojira export --jql "project = FOO" --json output.json
22+
23+
# Export to Excel
24+
gojira export --jql "project = FOO AND status = Open" --xlsx output.xlsx
25+
26+
# Export with parent issues included
27+
gojira export --jql "project = FOO" --include-parents --xlsx output.xlsx
28+
29+
# Export from existing JSON file to XLSX
30+
gojira export --from-json issues.json --xlsx output.xlsx
31+
32+
# Export specific issues by key
33+
gojira export --keys ISSUE-1,ISSUE-2,ISSUE-3 --json output.json`,
34+
RunE: runExport,
35+
}
36+
37+
var (
38+
exportJQL string
39+
exportKeys string
40+
exportJSONOutput string
41+
exportXLSXOutput string
42+
exportFromJSON string
43+
exportIncludeParents bool
44+
exportSheetName string
45+
)
46+
47+
func init() {
48+
rootCmd.AddCommand(exportCmd)
49+
50+
exportCmd.Flags().StringVar(&exportJQL, "jql", "", "JQL query to search issues")
51+
exportCmd.Flags().StringVar(&exportKeys, "keys", "", "Comma-separated issue keys to export")
52+
exportCmd.Flags().StringVar(&exportJSONOutput, "json", "", "Output JSON file path")
53+
exportCmd.Flags().StringVar(&exportXLSXOutput, "xlsx", "", "Output XLSX file path")
54+
exportCmd.Flags().StringVar(&exportFromJSON, "from-json", "", "Read issues from existing JSON file instead of querying")
55+
exportCmd.Flags().BoolVar(&exportIncludeParents, "include-parents", false, "Include parent issues in export")
56+
exportCmd.Flags().StringVar(&exportSheetName, "sheet", "issues", "Sheet name for XLSX export")
57+
}
58+
59+
func runExport(cmd *cobra.Command, args []string) error {
60+
// Validate output flags
61+
if exportJSONOutput == "" && exportXLSXOutput == "" {
62+
return fmt.Errorf("at least one output format required: --json or --xlsx")
63+
}
64+
65+
var issuesSet *rest.IssuesSet
66+
var err error
67+
68+
// Get issues from source
69+
if exportFromJSON != "" {
70+
// Read from existing JSON file
71+
issuesSet, err = rest.IssuesSetReadFileJSON(exportFromJSON)
72+
if err != nil {
73+
return fmt.Errorf("failed to read JSON file: %w", err)
74+
}
75+
if !flagQuiet {
76+
fmt.Fprintf(os.Stderr, "Loaded %d issues from %s\n", issuesSet.Len(), exportFromJSON)
77+
}
78+
} else {
79+
// Query from Jira
80+
issuesSet, err = fetchIssuesForExport()
81+
if err != nil {
82+
return err
83+
}
84+
}
85+
86+
// Include parents if requested
87+
if exportIncludeParents && exportFromJSON == "" {
88+
client, err := NewClientFromOptions(getAuthOptions())
89+
if err != nil {
90+
return fmt.Errorf("authentication failed: %w", err)
91+
}
92+
93+
if !flagQuiet {
94+
fmt.Fprintf(os.Stderr, "Fetching parent issues...\n")
95+
}
96+
if err := client.IssueAPI.IssuesSetAddParents(issuesSet); err != nil {
97+
return fmt.Errorf("failed to fetch parents: %w", err)
98+
}
99+
if !flagQuiet && issuesSet.Parents != nil {
100+
fmt.Fprintf(os.Stderr, "Added %d parent issues\n", len(issuesSet.Parents.Keys()))
101+
}
102+
}
103+
104+
// Export to JSON
105+
if exportJSONOutput != "" {
106+
if err := writeJSONExport(issuesSet, exportJSONOutput); err != nil {
107+
return err
108+
}
109+
}
110+
111+
// Export to XLSX
112+
if exportXLSXOutput != "" {
113+
if err := writeXLSXExport(issuesSet, exportXLSXOutput); err != nil {
114+
return err
115+
}
116+
}
117+
118+
return nil
119+
}
120+
121+
func fetchIssuesForExport() (*rest.IssuesSet, error) {
122+
if exportJQL == "" && exportKeys == "" {
123+
return nil, fmt.Errorf("query required: use --jql or --keys")
124+
}
125+
126+
client, err := NewClientFromOptions(getAuthOptions())
127+
if err != nil {
128+
return nil, fmt.Errorf("authentication failed: %w", err)
129+
}
130+
131+
if exportKeys != "" {
132+
// Fetch specific issues by key
133+
keys := parseKeys(exportKeys)
134+
if len(keys) == 0 {
135+
return nil, fmt.Errorf("no valid issue keys provided")
136+
}
137+
138+
if !flagQuiet {
139+
fmt.Fprintf(os.Stderr, "Fetching %d issues...\n", len(keys))
140+
}
141+
142+
issuesSet, err := client.IssueAPI.SearchIssuesSet(fmt.Sprintf("key in (%s)", strings.Join(keys, ",")))
143+
if err != nil {
144+
return nil, fmt.Errorf("failed to fetch issues: %w", err)
145+
}
146+
return issuesSet, nil
147+
}
148+
149+
// Search with JQL
150+
if !flagQuiet {
151+
fmt.Fprintf(os.Stderr, "Searching issues with JQL: %s\n", exportJQL)
152+
}
153+
154+
issuesSet, err := client.IssueAPI.SearchIssuesSet(exportJQL)
155+
if err != nil {
156+
return nil, fmt.Errorf("search failed: %w", err)
157+
}
158+
159+
if !flagQuiet {
160+
fmt.Fprintf(os.Stderr, "Found %d issues\n", issuesSet.Len())
161+
}
162+
163+
return issuesSet, nil
164+
}
165+
166+
func parseKeys(keysStr string) []string {
167+
var keys []string
168+
for _, k := range strings.Split(keysStr, ",") {
169+
k = strings.TrimSpace(k)
170+
if k != "" {
171+
keys = append(keys, k)
172+
}
173+
}
174+
return keys
175+
}
176+
177+
func writeJSONExport(issuesSet *rest.IssuesSet, outputPath string) error {
178+
// Ensure directory exists
179+
dir := filepath.Dir(outputPath)
180+
if dir != "" && dir != "." {
181+
if err := os.MkdirAll(dir, 0755); err != nil {
182+
return fmt.Errorf("failed to create directory: %w", err)
183+
}
184+
}
185+
186+
// Write JSON
187+
data, err := json.MarshalIndent(issuesSet, "", " ")
188+
if err != nil {
189+
return fmt.Errorf("failed to marshal JSON: %w", err)
190+
}
191+
192+
if err := os.WriteFile(outputPath, data, 0600); err != nil {
193+
return fmt.Errorf("failed to write JSON file: %w", err)
194+
}
195+
196+
if !flagQuiet {
197+
fmt.Fprintf(os.Stderr, "Wrote %d issues to %s\n", issuesSet.Len(), outputPath)
198+
}
199+
200+
return nil
201+
}
202+
203+
func writeXLSXExport(issuesSet *rest.IssuesSet, outputPath string) error {
204+
// Ensure directory exists
205+
dir := filepath.Dir(outputPath)
206+
if dir != "" && dir != "." {
207+
if err := os.MkdirAll(dir, 0755); err != nil {
208+
return fmt.Errorf("failed to create directory: %w", err)
209+
}
210+
}
211+
212+
// Generate table
213+
tbl, err := issuesSet.TableDefault(nil, true, "Top-level initiative", []string{})
214+
if err != nil {
215+
return fmt.Errorf("failed to generate table: %w", err)
216+
}
217+
218+
// Write XLSX
219+
if err := tbl.WriteXLSX(outputPath, exportSheetName); err != nil {
220+
return fmt.Errorf("failed to write XLSX: %w", err)
221+
}
222+
223+
if !flagQuiet {
224+
fmt.Fprintf(os.Stderr, "Wrote %d issues to %s\n", issuesSet.Len(), outputPath)
225+
}
226+
227+
return nil
228+
}

0 commit comments

Comments
 (0)