jiraexport.go (3346B)
1 // Command jiraexport prints the named Jira issues, and their comments, 2 // in RFC 5322 mail message format (email). 3 // 4 // Usage: 5 // 6 // jiraexport [ -d duration ] issue... 7 // 8 // The options are: 9 // 10 // -d duration 11 // Exclude any issues and comments unmodified since duration. 12 // Duration may be given in the format accepted by time.ParseDuration. 13 // For example, 24h (24 hours). The default is 7 days. 14 // -c 15 // Only print comments, excluding the issue. 16 // 17 // # Example 18 // 19 // Print the last day's updates to tickets SRE-1234 and SRE-5678: 20 // 21 // jiraexport -d 24h SRE-1234 SRE-5678 22 // 23 // Archive the first 500 tickets of a project in a mbox file: 24 // 25 // for i in `seq 1 500` 26 // do 27 // jiraexport TEST-$i >>issues.mbox 28 // done 29 package main 30 31 import ( 32 "flag" 33 "fmt" 34 "io" 35 "io/fs" 36 "log" 37 "net/mail" 38 "os" 39 "path" 40 "strings" 41 "time" 42 43 "olowe.co/issues/jira" 44 ) 45 46 func copyMessage(w io.Writer, msg *mail.Message) (n int, err error) { 47 for k, v := range msg.Header { 48 for i := range v { 49 nn, err := fmt.Fprintf(w, "%s: %s\n", k, v[i]) 50 n += nn 51 if err != nil { 52 return n, fmt.Errorf("write header field %s: %w", k, err) 53 } 54 } 55 } 56 nnn, err := fmt.Fprintln(w) 57 n += nnn 58 if err != nil { 59 return n, err 60 } 61 nn, err := io.Copy(w, msg.Body) 62 return n + int(nn), err 63 } 64 65 const usage string = "jiraexport [-d duration] [-u url] issue [...]" 66 67 var since = flag.Duration("d", 0, "exclude activity older than this duration") 68 var apiRoot = flag.String("u", "http://[::1]:8080", "base URL for the JIRA API") 69 var onlyComments = flag.Bool("c", false, "only print comments") 70 71 func init() { 72 log.SetFlags(0) 73 log.SetPrefix("") 74 flag.Parse() 75 } 76 77 func main() { 78 if len(flag.Args()) == 0 { 79 log.Fatal(usage) 80 } 81 82 confDir, err := os.UserConfigDir() 83 if err != nil { 84 log.Fatal(err) 85 } 86 confDir = path.Join(confDir, "atlassian/jira") 87 config, err := readConfig(confDir) 88 if err != nil { 89 log.Fatalln("read config:", err) 90 } 91 jclient := &jira.Client{ 92 APIRoot: config.BaseURL, 93 Username: config.Username, 94 Password: config.Password, 95 Debug: false, 96 } 97 fsys := &jira.FS{Client: jclient} 98 99 for _, arg := range flag.Args() { 100 proj, num, ok := strings.Cut(arg, "-") 101 if !ok { 102 log.Println("bad issue name: missing - separator") 103 continue 104 } 105 dir := path.Join(proj, num) 106 issue, err := fsys.Open(path.Join(dir, "issue")) 107 if err != nil { 108 log.Println(err) 109 continue 110 } 111 msg, err := mail.ReadMessage(issue) 112 if err != nil { 113 log.Println("read issue:", err) 114 issue.Close() 115 continue 116 } 117 subject := msg.Header.Get("Subject") 118 if !*onlyComments { 119 if _, err := copyMessage(os.Stdout, msg); err != nil { 120 log.Println("print issue:", err) 121 } 122 } 123 issue.Close() 124 125 dents, err := fs.ReadDir(fsys, dir) 126 if err != nil { 127 log.Println(err) 128 continue 129 } 130 for _, d := range dents { 131 if d.Name() == "issue" { 132 continue // already done 133 } 134 info, err := d.Info() 135 if err != nil { 136 log.Println(err) 137 continue 138 } 139 if *since != 0 && time.Since(info.ModTime()) >= *since { 140 continue 141 } 142 f, err := fsys.Open(path.Join(dir, d.Name())) 143 if err != nil { 144 log.Println(err) 145 continue 146 } 147 fmt.Println() 148 fmt.Println("From nobody", info.ModTime().Format(time.ANSIC)) 149 fmt.Println("Subject:", subject) 150 if _, err := io.Copy(os.Stdout, f); err != nil { 151 log.Println(err) 152 } 153 f.Close() 154 } 155 } 156 }