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 }