issues

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

commit be8ed6895c79783a5342d6be09e95a0521c90348
parent 005681d996a84d915ba539d33feee9f9a2b0b57b
Author: Oliver Lowe <o@olowe.co>
Date:   Fri, 31 Jul 2026 16:34:39 +1000

issue: initial search support

Diffstat:
Missue/acme.go | 51+++++++++++++++++++++++++++++++++++++++++----------
Missue/hub/file.go | 2+-
Missue/hub/hub.go | 41+++++++++++++++++++++++++++++++----------
3 files changed, 73 insertions(+), 21 deletions(-)

diff --git a/issue/acme.go b/issue/acme.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io/fs" - "log" "path" "strconv" "strings" @@ -29,11 +28,13 @@ func (w *awin) pathname() string { } func (w *awin) Look(text string) bool { + var name string if n, _ := strconv.Atoi(strings.TrimPrefix(text, "#")); 0 < n && n < 1000000 { - // found an issue, try loading it + name = fmt.Sprintf("%d/issue", n) + } else { + name = path.Join(w.pathname(), text) } - name := path.Join(w.pathname(), text) stat, err := fs.Stat(fsys, name) if errors.Is(err, fs.ErrNotExist) { return false @@ -99,6 +100,13 @@ func (w *awin) load() { const newIssueTemplate = `Title: ` func (w *awin) Execute(cmd string) bool { + github, ok := fsys.(*hub.FS) + if !ok { + msg := fmt.Sprintf("unexpected FS type %T", fsys) + w.Err(msg) + return false + } + switch cmd { case "Get": w.load() @@ -121,7 +129,6 @@ func (w *awin) Execute(cmd string) bool { 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 @@ -136,12 +143,6 @@ func (w *awin) Execute(cmd string) bool { 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) @@ -157,5 +158,35 @@ func (w *awin) Execute(cmd string) bool { go w.load() return true } + + if strings.HasPrefix(cmd, "Search") { + query := strings.TrimSpace(strings.TrimPrefix(cmd, "Search")) + owner, repo, ok := strings.Cut(project, "/") + if !ok { + w.Err("botch, invalid project name " + project) + return false + } + + win := new(awin) + w1, err := acme.New() + if err != nil { + w.Err(err.Error()) + return false + } + win.Win = w1 + win.Name("/issue/search") + win.Write("body", []byte("Search "+query+"\n\n")) + go win.EventLoop(win) + hits, err := github.Client.SearchIssues(owner, repo, query) + if err != nil { + w.Err(fmt.Sprintf("search %q: %v", query, err)) + return false + } + for _, issue := range hits { + win.Fprintf("body", "%d %s\n", issue.Number, issue.Title) + } + return true + } + return false } diff --git a/issue/hub/file.go b/issue/hub/file.go @@ -81,7 +81,7 @@ func (f *fakeFile) Stat() (fs.FileInfo, error) { if f.buf == nil { f.buf = buf } - _ = printIssue(buf, issue) + _ = printIssue(buf, issue) // cannot fail writing to Buffer st := stat{ name: f.Name(), mtime: issue.Updated, diff --git a/issue/hub/hub.go b/issue/hub/hub.go @@ -8,6 +8,7 @@ import ( "io" "io/fs" "net/http" + "net/url" "path" "strconv" "strings" @@ -102,12 +103,13 @@ func printComments(w io.Writer, comments []Comment) error { return err } +type Error struct { + Message string +} -func (c *Client) CheckIssue(owner, repo string, number int) (bool, error) { - if c.Client == nil { - c.Client = http.DefaultClient - } +func (e *Error) Error() string { return e.Message } +func (c *Client) CheckIssue(owner, repo string, number int) (bool, error) { p := path.Join("/repos", owner, repo, "issues", strconv.Itoa(number)) resp, err := c.head(p) if err != nil { @@ -136,14 +138,30 @@ func (c *Client) Issues(owner, repo string) ([]Issue, error) { return issues, err } -func (c *Client) CreateIssue(owner, repo, title, body string) (*Issue, error) { - if c.Client == nil { - c.Client = http.DefaultClient +func (c *Client) SearchIssues(owner, repo, query string) ([]Issue, error) { + if len(query) > 256 { + return nil, fmt.Errorf("query length %d longer than max 256", len(query)) } + q := fmt.Sprintf("type:issue repo:%s/%s %s", owner, repo, query) + reqPath := "/search/issues?per_page=50&q=" + url.QueryEscape(q) + hits := struct { + TotalCount int + Items []Issue + }{ + TotalCount: 0, + Items: make([]Issue, 0), + } + if err := c.get(reqPath, &hits); err != nil { + return nil, fmt.Errorf("execute search: %w", err) + } + return hits.Items, nil +} + +func (c *Client) CreateIssue(owner, repo, title, body string) (*Issue, error) { m := map[string]string{ "title": title, - "body": body, + "body": body, } b, err := json.Marshal(&m) if err != nil { @@ -192,8 +210,11 @@ func (c *Client) get(path string, v any) error { } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - // TODO: decode any error messages from body - return errors.New(resp.Status) + var e Error + if err := json.NewDecoder(resp.Body).Decode(&e); err != nil { + return fmt.Errorf("response status %s: decode error message: %w", resp.Status, err) + } + return &e } if err := json.NewDecoder(resp.Body).Decode(v); err != nil {