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:
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
+}