commit be8ed6895c79783a5342d6be09e95a0521c90348
parent 005681d996a84d915ba539d33feee9f9a2b0b57b
Author: Oliver Lowe <o@olowe.co>
Date: Fri, 31 Jul 2026 16:34:39 +1000
issue: initial search support
Diffstat:
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 {