issues

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

issue.go (2946B)


      1 package main
      2 
      3 import (
      4 	"bufio"
      5 	"bytes"
      6 	"errors"
      7 	"flag"
      8 	"fmt"
      9 	"io"
     10 	"io/fs"
     11 	"log"
     12 	"os"
     13 	"path/filepath"
     14 	"strings"
     15 
     16 	"9fans.net/go/acme"
     17 	"olowe.co/issues/issue/hub"
     18 )
     19 
     20 var (
     21 	projectFlag = flag.String("p", "", "GitHub owner/repo name")
     22 )
     23 
     24 var fsys fs.FS
     25 
     26 var project string = "golang/go"
     27 
     28 func usage() {
     29 	fmt.Fprintf(os.Stderr, "usage: issue [-p owner/repo]")
     30 	flag.PrintDefaults()
     31 	os.Exit(2)
     32 }
     33 
     34 func init() {
     35 	flag.Usage = usage
     36 	flag.Parse()
     37 	log.SetFlags(0)
     38 	log.SetPrefix("issue: ")
     39 }
     40 
     41 func main() {
     42 	confDir, err := os.UserConfigDir()
     43 	if err != nil {
     44 		log.Fatalln("find config dir:", err)
     45 	}
     46 	confDir = filepath.Join(confDir, "github")
     47 	if *projectFlag != "" {
     48 		project = *projectFlag
     49 	} else {
     50 		projPath := confDir + "/project"
     51 		if _, err := os.Stat(projPath); err == nil {
     52 			b, err := os.ReadFile(projPath)
     53 			if err != nil {
     54 				log.Println("read default project file:", err)
     55 			} else {
     56 				project = string(bytes.TrimSpace(b))
     57 			}
     58 		}
     59 	}
     60 	token, err := os.ReadFile(filepath.Join(confDir, "token"))
     61 	if errors.Is(err, fs.ErrNotExist) {
     62 		// continue unauthenticated
     63 	} else if err != nil {
     64 		log.Fatalf("load github auth token: %v", err)
     65 	}
     66 
     67 	owner, repo, ok := strings.Cut(project, "/")
     68 	if !ok {
     69 		log.Fatal("invalid project form: must be owner/repo, like golang/go")
     70 	}
     71 
     72 	fsys = &hub.FS{
     73 		Client: &hub.Client{
     74 			Token: string(token),
     75 		},
     76 		Owner: owner,
     77 		Repo:  repo,
     78 	}
     79 
     80 	var root awin
     81 	root.Win, err = acme.New()
     82 	if err != nil {
     83 		log.Fatalf("create root acme window: %v", err)
     84 	}
     85 	root.Name("/issue/")
     86 	root.load()
     87 	root.EventLoop(&root)
     88 }
     89 
     90 func wrap(t string, prefix string) string {
     91 	out := ""
     92 	t = strings.Replace(t, "\r\n", "\n", -1)
     93 	max := 70
     94 	lines := strings.Split(t, "\n")
     95 	for i, line := range lines {
     96 		if i > 0 {
     97 			out += "\n" + prefix
     98 		}
     99 		s := line
    100 		for len(s) > max {
    101 			i := strings.LastIndex(s[:max], " ")
    102 			if i < 0 {
    103 				i = max - 1
    104 			}
    105 			i++
    106 			out += s[:i] + "\n" + prefix
    107 			s = s[i:]
    108 		}
    109 		out += s
    110 	}
    111 	return out
    112 }
    113 
    114 func mustConfigPath() string {
    115 	dir, err := os.UserConfigDir()
    116 	if err != nil {
    117 		log.Fatal("find user configuration directory: ", err)
    118 	}
    119 	return dir
    120 }
    121 
    122 // readNewIssue reads from rd an issue in mail message format.
    123 // Only one entry in the header is permitted: "Title:".
    124 // The first line after the title is ignored.
    125 // Every subsequent line is considered part of the body.
    126 func readNewIssue(rd io.Reader) (title, body string, err error) {
    127 	buf := bufio.NewReader(rd)
    128 	title, err = buf.ReadString('\n')
    129 	if err != nil {
    130 		return title, body, fmt.Errorf("read title: %w", err)
    131 	}
    132 	title = strings.TrimPrefix(title, "Title: ")
    133 
    134 	if _, err := buf.ReadString('\n'); err != nil {
    135 		return title, body, fmt.Errorf("read line after title: %w", err)
    136 	}
    137 
    138 	builder := &strings.Builder{}
    139 	if _, err := io.Copy(builder, buf); err != nil {
    140 		return title, body, fmt.Errorf("read body: %w", err)
    141 	}
    142 	return strings.TrimSpace(title), strings.TrimSpace(builder.String()), nil
    143 }