issues

Github, Jira, Gitlab issues from Plan 9's acme
Log | Files | Refs | README | LICENSE

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 }