issues

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

Jira.go (5169B)


      1 package main
      2 
      3 import (
      4 	"errors"
      5 	"flag"
      6 	"fmt"
      7 	"io"
      8 	"io/fs"
      9 	"log"
     10 	"os"
     11 	"path"
     12 	"regexp"
     13 	"strings"
     14 
     15 	"9fans.net/go/acme"
     16 	"olowe.co/issues/jira"
     17 )
     18 
     19 func init() {
     20 	log.SetFlags(0)
     21 	log.SetPrefix("Jira: ")
     22 }
     23 
     24 type awin struct {
     25 	*acme.Win
     26 	fsys fs.FS
     27 }
     28 
     29 func (w *awin) name() string {
     30 	b, err := w.ReadAll("tag")
     31 	if err != nil {
     32 		w.Err(err.Error())
     33 		return ""
     34 	}
     35 	fields := strings.Fields(string(b))
     36 	return strings.TrimPrefix(fields[0], "/jira/")
     37 }
     38 
     39 var issueKeyExp = regexp.MustCompile("[A-Z]+-[0-9]+")
     40 
     41 func (w *awin) Look(text string) bool {
     42 	text = strings.TrimSpace(text)
     43 
     44 	var pathname string
     45 	if issueKeyExp.MatchString(text) {
     46 		proj, num, _ := strings.Cut(text, "-")
     47 		pathname = path.Join(proj, num, "issue")
     48 	} else {
     49 		pathname = path.Join(path.Dir(w.name()), text)
     50 		if strings.HasSuffix(text, "/") {
     51 			pathname = path.Join(w.name(), text)
     52 		}
     53 	}
     54 	f, err := w.fsys.Open(pathname)
     55 	if errors.Is(err, fs.ErrNotExist) {
     56 		return false
     57 	} else if err != nil {
     58 		w.Err(err.Error())
     59 		return false
     60 	}
     61 
     62 	win, err := acme.New()
     63 	if err != nil {
     64 		w.Err(err.Error())
     65 		return true
     66 	}
     67 
     68 	wname := path.Join("/jira", pathname)
     69 	var isDir bool
     70 	if d, ok := f.(fs.DirEntry); ok {
     71 		isDir = d.IsDir()
     72 	} else {
     73 		stat, err := f.Stat()
     74 		if err != nil {
     75 			w.Err(err.Error())
     76 			return true
     77 		}
     78 		isDir = stat.IsDir()
     79 	}
     80 	if isDir {
     81 		wname += "/"
     82 	}
     83 	win.Name(wname)
     84 
     85 	if path.Base(pathname) == "issue" {
     86 		win.Fprintf("tag", "New ")
     87 	}
     88 	ww := &awin{win, w.fsys}
     89 	go ww.EventLoop(ww)
     90 	go func() {
     91 		if err := ww.Get(f); err != nil {
     92 			w.Err(err.Error())
     93 		}
     94 		ww.Addr("#0")
     95 		ww.Ctl("dot=addr")
     96 		ww.Ctl("show")
     97 	}()
     98 	return true
     99 }
    100 
    101 func (w *awin) Execute(cmd string) bool {
    102 	fields := strings.Fields(strings.TrimSpace(cmd))
    103 	switch fields[0] {
    104 	case "Get":
    105 		if err := w.Get(nil); err != nil {
    106 			w.Err(err.Error())
    107 		}
    108 		return true
    109 	case "Search":
    110 		query := defaultSearch
    111 		if len(fields) > 1 {
    112 			query = strings.Join(fields[1:], " ")
    113 		}
    114 		go newSearch(w.fsys, query)
    115 		return true
    116 	case "New":
    117 		if len(fields) > 1 {
    118 			return false
    119 		}
    120 		win, err := acme.New()
    121 		if err != nil {
    122 			w.Err(err.Error())
    123 			return false
    124 		}
    125 		dname := path.Dir(w.name())
    126 		win.Name(path.Join("/jira", dname, "new"))
    127 		win.Fprintf("tag", "Put ")
    128 		a := &awin{win, w.fsys}
    129 		go a.EventLoop(a)
    130 		return true
    131 	case "Put":
    132 		if err := w.put(); err != nil {
    133 			w.Errf(err.Error())
    134 		} else {
    135 			w.Del(true)
    136 		}
    137 		return true
    138 	}
    139 	return false
    140 }
    141 
    142 func (w *awin) Get(f fs.File) error {
    143 	defer w.Ctl("clean")
    144 	fname := path.Clean(w.name())
    145 	if fname == "/" {
    146 		fname = "." // special name for the root file in io/fs
    147 	}
    148 	if f == nil {
    149 		var err error
    150 		f, err = w.fsys.Open(fname)
    151 		if err != nil {
    152 			return err
    153 		}
    154 	}
    155 
    156 	stat, err := f.Stat()
    157 	if err != nil {
    158 		return err
    159 	}
    160 	defer f.Close()
    161 	if stat.IsDir() {
    162 		dirs, err := f.(fs.ReadDirFile).ReadDir(-1)
    163 		if err != nil {
    164 			return err
    165 		}
    166 		buf := &strings.Builder{}
    167 		for _, d := range dirs {
    168 			if d.IsDir() {
    169 				fmt.Fprintln(buf, d.Name()+"/")
    170 				continue
    171 			}
    172 			fmt.Fprintln(buf, d.Name())
    173 		}
    174 		w.Clear()
    175 		w.PrintTabbed(buf.String())
    176 		return nil
    177 	}
    178 
    179 	b, err := io.ReadAll(f)
    180 	if err != nil {
    181 		return &fs.PathError{"read", stat.Name(), err}
    182 	}
    183 	w.Clear()
    184 	if _, err := w.Write("body", b); err != nil {
    185 		return fmt.Errorf("write body: %w", err)
    186 	}
    187 	return nil
    188 }
    189 
    190 const defaultSearch = "status != done and status != 'not done' and assignee = currentuser()"
    191 
    192 func newSearch(fsys fs.FS, query string) {
    193 	win, err := acme.New()
    194 	if err != nil {
    195 		acme.Errf("new window: %v", err.Error())
    196 		return
    197 	}
    198 	defer win.Ctl("clean")
    199 	win.Name("/jira/search")
    200 	f, ok := fsys.(*jira.FS)
    201 	if !ok {
    202 		win.Errf("cannot search with filesystem type %T", fsys)
    203 	}
    204 	win.PrintTabbed("Search " + query + "\n\n")
    205 	issues, err := f.Client.SearchIssues(query)
    206 	if err != nil {
    207 		win.Errf("search %q: %v", query, err)
    208 		return
    209 	}
    210 	win.PrintTabbed(printIssues(issues))
    211 	w := &awin{win, fsys}
    212 	go w.EventLoop(w)
    213 }
    214 
    215 func printIssues(issues []jira.Issue) string {
    216 	buf := &strings.Builder{}
    217 	for _, ii := range issues {
    218 		name := strings.Replace(ii.Key, "-", "/", 1)
    219 		fmt.Fprintf(buf, "%s/issue\t%s\n", name, ii.Summary)
    220 	}
    221 	return buf.String()
    222 }
    223 
    224 const usage string = "usage: Jira [-d]"
    225 
    226 var debug bool
    227 
    228 func init() {
    229 	flag.BoolVar(&debug, "d", false, "debug")
    230 	flag.Parse()
    231 }
    232 
    233 func main() {
    234 	home, err := os.UserHomeDir()
    235 	if err != nil {
    236 		log.Fatalf("find user config dir: %v", err)
    237 	}
    238 	credPath := path.Join(home, ".config/atlassian/jira")
    239 	if len(flag.Args()) == 1 {
    240 		credPath = flag.Args()[0]
    241 	} else if len(flag.Args()) > 2 {
    242 		fmt.Fprintln(os.Stderr, usage)
    243 		os.Exit(2)
    244 	}
    245 	config, err := readConfig(credPath)
    246 	if err != nil {
    247 		log.Fatalf("read configuration: %v", err)
    248 	}
    249 
    250 	// srv := newFakeServer("testdata")
    251 	// defer srv.Close()
    252 
    253 	fsys := &jira.FS{
    254 		Client: &jira.Client{
    255 			Debug:    debug,
    256 			APIRoot:  config.BaseURL,
    257 			Username: config.Username,
    258 			Password: config.Password,
    259 		},
    260 	}
    261 
    262 	acme.AutoExit(true)
    263 	win, err := acme.New()
    264 	if err != nil {
    265 		log.Fatal(err)
    266 	}
    267 	win.Name("/jira/")
    268 	root := &awin{win, fsys}
    269 	root.Get(nil)
    270 	root.Addr("#0")
    271 	root.Ctl("dot=addr")
    272 	root.Ctl("show")
    273 	win.Ctl("clean")
    274 	root.EventLoop(root)
    275 }