issues

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

commit 005681d996a84d915ba539d33feee9f9a2b0b57b
parent f89e6959eeefa36199b1e3da13833da91e6bc5bf
Author: Oliver Lowe <o@olowe.co>
Date:   Fri, 31 Jul 2026 13:47:04 +1000

issue: initial stab at issue creation

Diffstat:
Missue/acme.go | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Missue/hub/hub.go | 20++++++++++++++++++--
Missue/issue.go | 25+++++++++++++++++++++++++
3 files changed, 99 insertions(+), 10 deletions(-)

diff --git a/issue/acme.go b/issue/acme.go @@ -1,21 +1,17 @@ package main import ( + "bytes" "errors" + "fmt" "io/fs" + "log" "path" "strconv" "strings" "9fans.net/go/acme" -) - -const ( - modeSingle = 1 + iota - modeQuery - modeCreate - modeMilestone - modeBulk + "olowe.co/issues/issue/hub" ) type awin struct { @@ -100,14 +96,66 @@ func (w *awin) load() { w.Ctl("show") } +const newIssueTemplate = `Title: ` + func (w *awin) Execute(cmd string) bool { switch cmd { case "Get": w.load() + w.Ctl("clean") return true case "Del": w.Ctl("del") return true + case "New": + win := new(awin) + w1, err := acme.New() + if err != nil { + w.Err(err.Error()) + return false + } + + win.Win = w1 + win.Name("/issue/new") + win.Write("body", []byte(newIssueTemplate)) + go win.EventLoop(win) + return true + case "Put": + log.Println("ok here we go...") + if w.pathname() != "new" { + w.Err(fs.ErrPermission.Error()) + return false + } + b, err := w.ReadAll("body") + if err != nil { + w.Err(err.Error()) + return false + } + title, body, err := readNewIssue(bytes.NewReader(b)) + if err != nil { + w.Err("read issue: " + err.Error()) + return false + } + github, ok := fsys.(*hub.FS) + if !ok { + msg := fmt.Sprintf("unexpected FS type %T", fsys) + w.Err(msg) + return false + } + owner, repo, ok := strings.Cut(project, "/") + if !ok { + w.Err("botch, invalid project name " + project) + return false + } + issue, err := github.Client.CreateIssue(owner, repo, title, body) + if err != nil { + w.Err("create issue: " + err.Error()) + return false + } + name := fmt.Sprintf("/issue/%d/issue", issue.Number) + w.Name(name) + go w.load() + return true } return false } diff --git a/issue/hub/hub.go b/issue/hub/hub.go @@ -31,7 +31,7 @@ type Issue struct { Assignee struct { Name string `json:"login"` } - Labels []string + Labels []Label State string Created time.Time `json:"created_at"` Updated time.Time `json:"updated_at"` @@ -41,6 +41,11 @@ type Issue struct { Comments int } +type Label struct { + Name string + Description string +} + func printIssue(w io.Writer, issue *Issue) error { buf := &bytes.Buffer{} fmt.Fprintln(buf, "Title:", issue.Title) @@ -51,7 +56,13 @@ func printIssue(w io.Writer, issue *Issue) error { fmt.Fprintln(buf, "Closed:", issue.Closed.Format(time.DateTime)) } fmt.Fprintln(buf, "Assignee:", issue.Assignee.Name) - fmt.Fprintln(buf, "Labels:", strings.Join(issue.Labels, " ")) + + labels := make([]string, len(issue.Labels)) + for i := range issue.Labels { + labels[i] = issue.Labels[i].Name + } + fmt.Fprintln(buf, "Labels:", strings.Join(labels, ", ")) + fmt.Fprintln(buf, "URL:", issue.HTMLURL) fmt.Fprintln(buf) fmt.Fprintln(buf, strings.TrimSpace(issue.Body)) @@ -91,6 +102,7 @@ func printComments(w io.Writer, comments []Comment) error { return err } + func (c *Client) CheckIssue(owner, repo string, number int) (bool, error) { if c.Client == nil { c.Client = http.DefaultClient @@ -125,6 +137,10 @@ func (c *Client) Issues(owner, repo string) ([]Issue, error) { } func (c *Client) CreateIssue(owner, repo, title, body string) (*Issue, error) { + if c.Client == nil { + c.Client = http.DefaultClient + } + m := map[string]string{ "title": title, "body": body, diff --git a/issue/issue.go b/issue/issue.go @@ -1,10 +1,12 @@ package main import ( + "bufio" "bytes" "errors" "flag" "fmt" + "io" "io/fs" "log" "os" @@ -116,3 +118,26 @@ func mustConfigPath() string { } return dir } + +// readNewIssue reads from rd an issue in mail message format. +// Only one entry in the header is permitted: "Title:". +// The first line after the title is ignored. +// Every subsequent line is considered part of the body. +func readNewIssue(rd io.Reader) (title, body string, err error) { + buf := bufio.NewReader(rd) + title, err = buf.ReadString('\n') + if err != nil { + return title, body, fmt.Errorf("read title: %w", err) + } + title = strings.TrimPrefix(title, "Title: ") + + if _, err := buf.ReadString('\n'); err != nil { + return title, body, fmt.Errorf("read line after title: %w", err) + } + + builder := &strings.Builder{} + if _, err := io.Copy(builder, buf); err != nil { + return title, body, fmt.Errorf("read body: %w", err) + } + return strings.TrimSpace(title), strings.TrimSpace(builder.String()), nil +}