commit d1235f3a5e08fd4516f6c854113dfa3e02cba2f3
parent 543888eeaf89c38d330d82a92d8336bfac1b5b1d
Author: Russ Cox <rsc@golang.org>
Date: Wed, 19 Jun 2019 15:54:09 -0400
issue: various updates
Updates from a long period of time.
Too much to describe in full.
Works better now. :-)
Diffstat:
| M | issue/acme.go | | | 605 | +++++++++++++++++++++++++++++++------------------------------------------------ |
| M | issue/edit.go | | | 82 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- |
| M | issue/issue.go | | | 178 | ++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------- |
3 files changed, 407 insertions(+), 458 deletions(-)
diff --git a/issue/acme.go b/issue/acme.go
@@ -7,23 +7,37 @@
package main
import (
+ "bufio"
"bytes"
+ "context"
"flag"
"fmt"
"log"
"os"
"regexp"
- "sort"
"strconv"
"strings"
"sync"
"time"
"9fans.net/go/acme"
- "9fans.net/go/draw"
+ "9fans.net/go/plumb"
"github.com/google/go-github/github"
)
+func (w *awin) project() string {
+ p := w.prefix
+ p = strings.TrimPrefix(p, "/issue/")
+ i := strings.Index(p, "/")
+ if i >= 0 {
+ j := strings.Index(p[i+1:], "/")
+ if j >= 0 {
+ p = p[:i+1+j]
+ }
+ }
+ return p
+}
+
func acmeMode() {
var dummy awin
dummy.prefix = "/issue/" + *project + "/"
@@ -32,21 +46,76 @@ func acmeMode() {
// Decide which behavior should be used, and use it consistently.
// TODO(rsc): Block this look from doing the multiline selection mode?
for _, arg := range flag.Args() {
- if dummy.look(arg) {
+ if dummy.Look(arg) {
continue
}
if arg == "new" {
dummy.createIssue()
continue
}
- dummy.newSearch("search", arg)
+ dummy.newSearch(dummy.prefix, "search", arg)
}
} else {
- dummy.look("all")
+ dummy.Look("all")
}
+
+ go dummy.plumbserve()
+
select {}
}
+func (w *awin) plumbserve() {
+ fid, err := plumb.Open("githubissue", 0)
+ if err != nil {
+ w.Err(fmt.Sprintf("plumb: %v", err))
+ return
+ }
+ r := bufio.NewReader(fid)
+ for {
+ var m plumb.Message
+ if err := m.Recv(r); err != nil {
+ w.Err(fmt.Sprintf("plumb recv: %v", err))
+ return
+ }
+ if m.Type != "text" {
+ w.Err(fmt.Sprintf("plumb recv: unexpected type: %s\n", m.Type))
+ continue
+ }
+ if m.Dst != "githubissue" {
+ w.Err(fmt.Sprintf("plumb recv: unexpected dst: %s\n", m.Dst))
+ continue
+ }
+ // TODO use m.Dir
+ data := string(m.Data)
+ var project, what string
+ if strings.HasPrefix(data, "/issue/") {
+ project = data[len("/issue/"):]
+ i := strings.LastIndex(project, "/")
+ if i < 0 {
+ w.Err(fmt.Sprintf("plumb recv: bad text %q", data))
+ continue
+ }
+ project, what = project[:i], project[i+1:]
+ } else {
+ i := strings.Index(data, "#")
+ if i < 0 {
+ w.Err(fmt.Sprintf("plumb recv: bad text %q", data))
+ continue
+ }
+ project, what = data[:i], data[i+1:]
+ }
+ if strings.Count(project, "/") != 1 {
+ w.Err(fmt.Sprintf("plumb recv: bad text %q", data))
+ continue
+ }
+ var plummy awin
+ plummy.prefix = "/issue/" + project + "/"
+ if !plummy.Look(what) {
+ w.Err(fmt.Sprintf("plumb recv: can't look %s%s", plummy.prefix, what))
+ }
+ }
+}
+
const (
modeSingle = 1 + iota
modeQuery
@@ -62,37 +131,31 @@ type awin struct {
query string
id int
github *github.Issue
- tab int
- font *draw.Font
- fontName string
title string
sortByNumber bool // otherwise sort by title
}
var all struct {
sync.Mutex
- m map[string]*awin
- f map[string]*draw.Font
- numwin int
+ m map[*acme.Win]*awin
}
func (w *awin) exit() {
all.Lock()
defer all.Unlock()
- if all.m[w.title] == w {
- delete(all.m, w.title)
+ if all.m[w.Win] == w {
+ delete(all.m, w.Win)
}
- if all.numwin--; all.numwin == 0 {
+ if len(all.m) == 0 {
os.Exit(0)
}
}
-func (w *awin) new(title string) *awin {
+func (w *awin) new(prefix, title string) *awin {
all.Lock()
defer all.Unlock()
- all.numwin++
if all.m == nil {
- all.m = make(map[string]*awin)
+ all.m = make(map[*acme.Win]*awin)
}
w1 := new(awin)
w1.title = title
@@ -106,133 +169,106 @@ func (w *awin) new(title string) *awin {
log.Fatalf("creating acme window again: %v", err)
}
}
- w1.prefix = w.prefix
+ w1.prefix = prefix
+ w1.SetErrorPrefix(w1.prefix)
w1.Name(w1.prefix + title)
- if title != "new" {
- all.m[title] = w1
- }
+ all.m[w1.Win] = w1
return w1
}
-func (w *awin) show(title string) *awin {
- all.Lock()
- defer all.Unlock()
- if w1 := all.m[title]; w1 != nil {
- w.Ctl("show")
- return w1
- }
- return nil
-}
-
-func (w *awin) fixfont() {
- ctl := make([]byte, 1000)
- w.Seek("ctl", 0, 0)
- n, err := w.Read("ctl", ctl)
- if err != nil {
- return
- }
- f := strings.Fields(string(ctl[:n]))
- if len(f) < 8 {
- return
- }
- w.tab, _ = strconv.Atoi(f[7])
- if w.tab == 0 {
- return
- }
- name := f[6]
- if w.fontName == name {
- return
- }
- all.Lock()
- defer all.Unlock()
- if font := all.f[name]; font != nil {
- w.font = font
- w.fontName = name
- return
- }
- var disp *draw.Display = nil
- font, err := disp.OpenFont(name)
- if err != nil {
- return
- }
- if all.f == nil {
- all.f = make(map[string]*draw.Font)
- }
- all.f[name] = font
- w.font = font
+func (w *awin) show(title string) bool {
+ return acme.Show(w.prefix+title) != nil
}
var numRE = regexp.MustCompile(`(?m)^#[0-9]+\t`)
+var repoHashRE = regexp.MustCompile(`\A([A-Za-z0-9_]+/[A-Za-z0-9_]+)#(all|[0-9]+)\z`)
var milecache struct {
sync.Mutex
- list []*github.Milestone
+ list map[string][]*github.Milestone
}
-func cachedMilestones() []*github.Milestone {
+func cachedMilestones(project string) []*github.Milestone {
milecache.Lock()
if milecache.list == nil {
- milecache.list, _ = loadMilestones()
+ milecache.list = make(map[string][]*github.Milestone)
+ }
+ if milecache.list[project] == nil {
+ milecache.list[project], _ = loadMilestones(project)
}
- list := milecache.list
+ list := milecache.list[project]
milecache.Unlock()
return list
}
-func (w *awin) look(text string) bool {
+func (w *awin) Look(text string) bool {
ids := readBulkIDs([]byte(text))
if len(ids) > 0 {
for _, id := range ids {
text := fmt.Sprint(id)
- if w.show(text) != nil {
+ if w.show(text) {
continue
}
- w.newIssue(text, id)
+ w.newIssue(w.prefix, text, id)
}
return true
}
if text == "all" {
- if w.show("all") != nil {
+ if w.show("all") {
return true
}
- w.newSearch("all", "")
+ w.newSearch(w.prefix, "all", "")
return true
}
if text == "Milestone" || text == "Milestones" || text == "milestone" {
- if w.show("milestone") != nil {
+ if w.show("milestone") {
return true
}
w.newMilestoneList()
return true
}
- milecache.Lock()
- if milecache.list == nil {
- milecache.list, _ = loadMilestones()
- }
- list := milecache.list
- milecache.Unlock()
+ list := cachedMilestones(w.project())
for _, m := range list {
if getString(m.Title) == text {
- if w.show(text) != nil {
+ if w.show(text) {
return true
}
- w.newSearch(text, "milestone:"+text)
+ w.newSearch(w.prefix, text, "milestone:"+text)
return true
}
}
- if n, _ := strconv.Atoi(strings.TrimPrefix(text, "#")); 0 < n && n < 100000 {
+ if n, _ := strconv.Atoi(strings.TrimPrefix(text, "#")); 0 < n && n < 1000000 {
text = strings.TrimPrefix(text, "#")
- if w.show(text) != nil {
+ if w.show(text) {
return true
}
- w.newIssue(text, n)
+ w.newIssue(w.prefix, text, n)
return true
}
+
+ if m := repoHashRE.FindStringSubmatch(text); m != nil {
+ project := m[1]
+ what := m[2]
+ prefix := "/issue/" + project + "/"
+ if acme.Show(prefix+what) != nil {
+ return true
+ }
+ if what == "all" {
+ w.newSearch(prefix, what, "")
+ return true
+ }
+ if n, _ := strconv.Atoi(what); 0 < n && n < 1000000 {
+ w.newIssue(prefix, what, n)
+ return true
+ }
+ return false
+ }
+
if m := numRE.FindAllString(text, -1); m != nil {
for _, s := range m {
- w.look(strings.TrimSpace(strings.TrimPrefix(s, "#")))
+ w.Look(strings.TrimSpace(strings.TrimPrefix(s, "#")))
}
return true
}
@@ -241,16 +277,16 @@ func (w *awin) look(text string) bool {
func (w *awin) setMilestone(milestone, text string) {
var buf bytes.Buffer
- id := findMilestone(&buf, &milestone)
+ id := findMilestone(&buf, w.project(), &milestone)
if buf.Len() > 0 {
- w.err(strings.TrimSpace(buf.String()))
+ w.Err(strings.TrimSpace(buf.String()))
}
if id == nil {
return
}
milestoneID := *id
- stop := w.blinker()
+ stop := w.Blink()
defer stop()
if w.mode == modeSingle {
w.setMilestone1(milestoneID, w.id)
@@ -276,14 +312,14 @@ func (w *awin) setMilestone1(milestoneID, n int) {
var edit github.IssueRequest
edit.Milestone = &milestoneID
- _, _, err := client.Issues.Edit(projectOwner, projectRepo, n, &edit)
+ _, _, err := client.Issues.Edit(context.TODO(), projectOwner(w.project()), projectRepo(w.project()), n, &edit)
if err != nil {
- w.err(fmt.Sprintf("Error changing issue #%d: %v", n, err))
+ w.Err(fmt.Sprintf("Error changing issue #%d: %v", n, err))
}
}
func (w *awin) createIssue() {
- w = w.new("new")
+ w = w.new(w.prefix, "new")
w.mode = modeCreate
w.Ctl("cleartag")
w.Fprintf("tag", " Put Search ")
@@ -291,8 +327,8 @@ func (w *awin) createIssue() {
go w.loop()
}
-func (w *awin) newIssue(title string, id int) {
- w = w.new(title)
+func (w *awin) newIssue(prefix, title string, id int) {
+ w = w.new(prefix, title)
w.mode = modeSingle
w.id = id
w.Ctl("cleartag")
@@ -302,7 +338,7 @@ func (w *awin) newIssue(title string, id int) {
}
func (w *awin) newBulkEdit(body []byte) {
- w = w.new("bulk-edit/")
+ w = w.new(w.prefix, "bulk-edit/")
w.mode = modeBulk
w.query = ""
w.Ctl("cleartag")
@@ -313,7 +349,7 @@ func (w *awin) newBulkEdit(body []byte) {
}
func (w *awin) newMilestoneList() {
- w = w.new("milestone")
+ w = w.new(w.prefix, "milestone")
w.mode = modeMilestone
w.query = ""
w.Ctl("cleartag")
@@ -323,8 +359,8 @@ func (w *awin) newMilestoneList() {
go w.loop()
}
-func (w *awin) newSearch(title, query string) {
- w = w.new(title)
+func (w *awin) newSearch(prefix, title, query string) {
+ w = w.new(prefix, title)
w.mode = modeQuery
w.query = query
w.Ctl("cleartag")
@@ -334,65 +370,28 @@ func (w *awin) newSearch(title, query string) {
go w.loop()
}
-func (w *awin) blinker() func() {
- c := make(chan struct{})
- go func() {
- t := time.NewTicker(1000 * time.Millisecond)
- defer t.Stop()
- dirty := false
- for {
- select {
- case <-t.C:
- dirty = !dirty
- if dirty {
- w.Ctl("dirty")
- } else {
- w.Ctl("clean")
- }
- case <-c:
- if dirty {
- w.Ctl("clean")
- }
- c <- struct{}{}
- return
- }
- }
- }()
- return func() {
- c <- struct{}{}
- <-c
- }
-}
-
-func (w *awin) clear() {
- w.Addr(",")
- w.Write("data", nil)
-}
-
-var createTemplate = `Title:
-Assignee:
-Labels:
-Milestone:
+var createTemplate = `Title:
+Assignee:
+Labels:
+Milestone:
<describe issue here>
`
func (w *awin) load() {
- w.fixfont()
-
switch w.mode {
case modeCreate:
- w.clear()
+ w.Clear()
w.Write("body", []byte(createTemplate))
w.Ctl("clean")
case modeSingle:
var buf bytes.Buffer
- stop := w.blinker()
- issue, err := showIssue(&buf, w.id)
+ stop := w.Blink()
+ issue, err := showIssue(&buf, w.project(), w.id)
stop()
- w.clear()
+ w.Clear()
if err != nil {
w.Write("body", []byte(err.Error()))
break
@@ -402,13 +401,16 @@ func (w *awin) load() {
w.github = issue
case modeMilestone:
- stop := w.blinker()
- milestones, err := loadMilestones()
+ stop := w.Blink()
+ milestones, err := loadMilestones(w.project())
milecache.Lock()
- milecache.list = milestones
+ if milecache.list == nil {
+ milecache.list = make(map[string][]*github.Milestone)
+ }
+ milecache.list[w.project()] = milestones
milecache.Unlock()
stop()
- w.clear()
+ w.Clear()
if err != nil {
w.Fprintf("body", "Error loading milestones: %v\n", err)
break
@@ -417,25 +419,25 @@ func (w *awin) load() {
for _, m := range milestones {
fmt.Fprintf(&buf, "%s\t%s\t%d\n", getTime(m.DueOn).Format("2006-01-02"), getString(m.Title), getInt(m.OpenIssues))
}
- w.printTabbed(buf.String())
+ w.PrintTabbed(buf.String())
w.Ctl("clean")
case modeQuery:
var buf bytes.Buffer
- stop := w.blinker()
- err := showQuery(&buf, w.query)
+ stop := w.Blink()
+ err := showQuery(&buf, w.project(), w.query)
if w.title == "all" {
- cachedMilestones()
+ cachedMilestones(w.project())
}
stop()
- w.clear()
+ w.Clear()
if err != nil {
w.Write("body", []byte(err.Error()))
break
}
if w.title == "all" {
var names []string
- for _, m := range cachedMilestones() {
+ for _, m := range cachedMilestones(w.project()) {
names = append(names, getString(m.Title))
}
if len(names) > 0 {
@@ -445,25 +447,25 @@ func (w *awin) load() {
if w.title == "search" {
w.Fprintf("body", "Search %s\n\n", w.query)
}
- w.printTabbed(buf.String())
+ w.PrintTabbed(buf.String())
w.Ctl("clean")
case modeBulk:
- stop := w.blinker()
+ stop := w.Blink()
body, err := w.ReadAll("body")
if err != nil {
- w.err(fmt.Sprintf("%v", err))
+ w.Err(fmt.Sprintf("%v", err))
stop()
break
}
- base, original, err := bulkEditStartFromText(body)
+ base, original, err := bulkEditStartFromText(w.project(), body)
stop()
if err != nil {
- w.err(fmt.Sprintf("%v", err))
+ w.Err(fmt.Sprintf("%v", err))
break
}
- w.clear()
- w.printTabbed(string(original))
+ w.Clear()
+ w.PrintTabbed(string(original))
w.Ctl("clean")
w.github = base
}
@@ -473,20 +475,6 @@ func (w *awin) load() {
w.Ctl("show")
}
-func (w *awin) err(s string) {
- if !strings.HasSuffix(s, "\n") {
- s = s + "\n"
- }
- w1 := w.show("+Errors")
- if w1 == nil {
- w1 = w.new("+Errors")
- }
- w1.Fprintf("body", "%s", s)
- w1.Addr("$")
- w1.Ctl("dot=addr")
- w1.Ctl("show")
-}
-
func diff(line, field, old string) *string {
old = strings.TrimSpace(old)
line = strings.TrimSpace(strings.TrimPrefix(line, field))
@@ -497,7 +485,7 @@ func diff(line, field, old string) *string {
}
func (w *awin) put() {
- stop := w.blinker()
+ stop := w.Blink()
defer stop()
switch w.mode {
case modeSingle, modeCreate:
@@ -507,12 +495,12 @@ func (w *awin) put() {
}
data, err := w.ReadAll("body")
if err != nil {
- w.err(fmt.Sprintf("Put: %v", err))
+ w.Err(fmt.Sprintf("Put: %v", err))
return
}
- issue, err := writeIssue(old, data, false)
+ issue, _, err := writeIssue(w.project(), old, data, false)
if err != nil {
- w.err(err.Error())
+ w.Err(err.Error())
return
}
if w.mode == modeCreate {
@@ -520,9 +508,6 @@ func (w *awin) put() {
w.id = getInt(issue.Number)
w.title = fmt.Sprint(w.id)
w.Name(w.prefix + w.title)
- all.Lock()
- all.m[w.title] = w
- all.Unlock()
w.github = issue
}
w.load()
@@ -530,84 +515,47 @@ func (w *awin) put() {
case modeBulk:
data, err := w.ReadAll("body")
if err != nil {
- w.err(fmt.Sprintf("Put: %v", err))
+ w.Err(fmt.Sprintf("Put: %v", err))
return
}
- ids, err := bulkWriteIssue(w.github, data, func(s string) { w.err("Put: " + s) })
+ ids, err := bulkWriteIssue(w.project(), w.github, data, func(s string) { w.Err("Put: " + s) })
if err != nil {
errText := strings.Replace(err.Error(), "\n", "\t\n", -1)
if len(ids) > 0 {
- w.err(fmt.Sprintf("updated %d issue%s with errors:\n\t%v", len(ids), suffix(len(ids)), errText))
+ w.Err(fmt.Sprintf("updated %d issue%s with errors:\n\t%v", len(ids), suffix(len(ids)), errText))
break
}
- w.err(fmt.Sprintf("%s", errText))
+ w.Err(fmt.Sprintf("%s", errText))
break
}
- w.err(fmt.Sprintf("updated %d issue%s", len(ids), suffix(len(ids))))
+ w.Err(fmt.Sprintf("updated %d issue%s", len(ids), suffix(len(ids))))
case modeMilestone:
- w.err("cannot Put milestone list")
+ w.Err("cannot Put milestone list")
case modeQuery:
- w.err("cannot Put issue list")
- }
-}
-
-func (w *awin) loadText(e *acme.Event) {
- if len(e.Text) == 0 && e.Q0 < e.Q1 {
- w.Addr("#%d,#%d", e.Q0, e.Q1)
- data, err := w.ReadAll("xdata")
- if err != nil {
- w.err(err.Error())
- }
- e.Text = data
- }
-}
-
-func (w *awin) selection() string {
- w.Ctl("addr=dot")
- data, err := w.ReadAll("xdata")
- if err != nil {
- w.err(err.Error())
+ w.Err("cannot Put issue list")
}
- return string(data)
}
func (w *awin) sort() {
if err := w.Addr("0/^[0-9]/,"); err != nil {
- w.err("nothing to sort")
- }
- data, err := w.ReadAll("xdata")
- if err != nil {
- w.err(err.Error())
- return
- }
- suffix := ""
- lines := strings.Split(string(data), "\n")
- if lines[len(lines)-1] == "" {
- suffix = "\n"
- lines = lines[:len(lines)-1]
+ w.Err("nothing to sort")
}
+ var less func(string, string) bool
if w.sortByNumber {
- sort.Stable(byNumber(lines))
+ less = func(x, y string) bool { return lineNumber(x) > lineNumber(y) }
} else {
- sort.Stable(bySecondField(lines))
+ less = func(x, y string) bool { return skipField(x) < skipField(y) }
+ }
+ if err := w.Sort(less); err != nil {
+ w.Err(err.Error())
}
- w.Addr("0/^[0-9]/,")
- w.Write("data", []byte(strings.Join(lines, "\n")+suffix))
w.Addr("0")
w.Ctl("dot=addr")
w.Ctl("show")
}
-type byNumber []string
-
-func (x byNumber) Len() int { return len(x) }
-func (x byNumber) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
-func (x byNumber) Less(i, j int) bool {
- return lineNumber(x[i]) > lineNumber(x[j])
-}
-
func lineNumber(s string) int {
n := 0
for j := 0; j < len(s) && '0' <= s[j] && s[j] <= '9'; j++ {
@@ -616,14 +564,6 @@ func lineNumber(s string) int {
return n
}
-type bySecondField []string
-
-func (x bySecondField) Len() int { return len(x) }
-func (x bySecondField) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
-func (x bySecondField) Less(i, j int) bool {
- return skipField(x[i]) < skipField(x[j])
-}
-
func skipField(s string) string {
i := strings.Index(s, "\t")
if i < 0 {
@@ -635,140 +575,61 @@ func skipField(s string) string {
return s[i:]
}
-func (w *awin) loop() {
- defer w.exit()
- for e := range w.EventChan() {
- switch e.C2 {
- case 'x', 'X': // execute
- cmd := strings.TrimSpace(string(e.Text))
- if cmd == "Get" {
- w.load()
- break
- }
- if cmd == "Put" {
- w.put()
- break
- }
- if cmd == "Del" {
- w.Ctl("del")
- break
- }
- if cmd == "New" {
- w.createIssue()
- break
- }
- if cmd == "Sort" {
- if w.mode != modeQuery {
- w.err("can only sort issue list windows")
- break
- }
- w.sortByNumber = !w.sortByNumber
- w.sort()
- break
- }
- if cmd == "Bulk" {
- // TODO(rsc): If Bulk has an argument, treat as search query and use results?
- if w.mode != modeQuery {
- w.err("can only start bulk edit in issue list windows")
- break
- }
- text := w.selection()
- if text == "" {
- data, err := w.ReadAll("body")
- if err != nil {
- w.err(fmt.Sprintf("%v", err))
- break
- }
- text = string(data)
- }
- w.newBulkEdit([]byte(text))
- break
- }
- if strings.HasPrefix(cmd, "Search ") {
- w.newSearch("search", strings.TrimSpace(strings.TrimPrefix(cmd, "Search")))
- break
- }
- if strings.HasPrefix(cmd, "Milestone ") {
- text := w.selection()
- w.setMilestone(strings.TrimSpace(strings.TrimPrefix(cmd, "Milestone")), text)
- break
- }
- w.WriteEvent(e)
- case 'l', 'L': // look
- // TODO(rsc): Expand selection, especially for URLs.
- w.loadText(e)
- if !w.look(string(e.Text)) {
- w.WriteEvent(e)
- }
- }
- }
-}
-
-func (w *awin) printTabbed(text string) {
- lines := strings.SplitAfter(text, "\n")
- var allRows [][]string
- for _, line := range lines {
- if line == "" {
- continue
- }
- line = strings.TrimSuffix(line, "\n")
- allRows = append(allRows, strings.Split(line, "\t"))
- }
-
- var buf bytes.Buffer
- for len(allRows) > 0 {
- if row := allRows[0]; len(row) <= 1 {
- if len(row) > 0 {
- buf.WriteString(row[0])
- }
- buf.WriteString("\n")
- allRows = allRows[1:]
- continue
+func (w *awin) Execute(cmd string) bool {
+ switch cmd {
+ case "Get":
+ w.load()
+ return true
+ case "Put":
+ w.put()
+ return true
+ case "Del":
+ w.Ctl("del")
+ return true
+ case "New":
+ w.createIssue()
+ return true
+ case "Sort":
+ if w.mode != modeQuery {
+ w.Err("can only sort issue list windows")
+ break
}
-
- i := 0
- for i < len(allRows) && len(allRows[i]) > 1 {
- i++
+ w.sortByNumber = !w.sortByNumber
+ w.sort()
+ return true
+ case "Bulk":
+ // TODO(rsc): If Bulk has an argument, treat as search query and use results?
+ if w.mode != modeQuery {
+ w.Err("can only start bulk edit in issue list windows")
+ return true
}
-
- rows := allRows[:i]
- allRows = allRows[i:]
-
- var wid []int
-
- if w.font != nil {
- for _, row := range rows {
- for len(wid) < len(row) {
- wid = append(wid, 0)
- }
- for i, col := range row {
- n := w.font.StringWidth(col)
- if wid[i] < n {
- wid[i] = n
- }
- }
+ text := w.Selection()
+ if text == "" {
+ data, err := w.ReadAll("body")
+ if err != nil {
+ w.Err(fmt.Sprintf("%v", err))
+ return true
}
+ text = string(data)
}
+ w.newBulkEdit([]byte(text))
+ return true
+ }
- for _, row := range rows {
- for i, col := range row {
- buf.WriteString(col)
- if i == len(row)-1 {
- break
- }
- if w.font == nil || w.tab == 0 {
- buf.WriteString("\t")
- continue
- }
- pos := w.font.StringWidth(col)
- for pos <= wid[i] {
- buf.WriteString("\t")
- pos += w.tab - pos%w.tab
- }
- }
- buf.WriteString("\n")
- }
+ if strings.HasPrefix(cmd, "Search ") {
+ w.newSearch(w.prefix, "search", strings.TrimSpace(strings.TrimPrefix(cmd, "Search")))
+ return true
+ }
+ if strings.HasPrefix(cmd, "Milestone ") {
+ text := w.Selection()
+ w.setMilestone(strings.TrimSpace(strings.TrimPrefix(cmd, "Milestone")), text)
+ return true
}
- w.Write("body", buf.Bytes())
+ return false
+}
+
+func (w *awin) loop() {
+ defer w.exit()
+ w.EventLoop(w)
}
diff --git a/issue/edit.go b/issue/edit.go
@@ -6,6 +6,7 @@ package main
import (
"bytes"
+ "context"
"errors"
"fmt"
"io"
@@ -20,21 +21,21 @@ import (
"github.com/google/go-github/github"
)
-func editIssue(original []byte, issue *github.Issue) {
+func editIssue(project string, original []byte, issue *github.Issue) {
updated := editText(original)
if bytes.Equal(original, updated) {
log.Print("no changes made")
return
}
- newIssue, err := writeIssue(issue, updated, false)
+ newIssue, _, err := writeIssue(project, issue, updated, false)
if err != nil {
log.Fatal(err)
}
if newIssue != nil {
issue = newIssue
}
- log.Printf("https://github.com/%s/issues/%d updated", *project, getInt(issue.Number))
+ log.Printf("https://github.com/%s/issues/%d updated", project, getInt(issue.Number))
}
func editText(original []byte) []byte {
@@ -90,7 +91,7 @@ func runEditor(filename string) error {
const bulkHeader = "\nBulk editing these issues:"
-func writeIssue(old *github.Issue, updated []byte, isBulk bool) (issue *github.Issue, err error) {
+func writeIssue(project string, old *github.Issue, updated []byte, isBulk bool) (issue *github.Issue, rate *github.Rate, err error) {
var errbuf bytes.Buffer
defer func() {
if errbuf.Len() > 0 {
@@ -132,7 +133,7 @@ func writeIssue(old *github.Issue, updated []byte, isBulk bool) (issue *github.I
}
case strings.HasPrefix(line, "Milestone:"):
- edit.Milestone = findMilestone(&errbuf, diff(line, "Milestone:", getMilestoneTitle(old.Milestone)))
+ edit.Milestone = findMilestone(&errbuf, project, diff(line, "Milestone:", getMilestoneTitle(old.Milestone)))
case strings.HasPrefix(line, "URL:"):
continue
@@ -143,23 +144,26 @@ func writeIssue(old *github.Issue, updated []byte, isBulk bool) (issue *github.I
}
if errbuf.Len() > 0 {
- return nil, nil
+ return nil, nil, nil
}
if getInt(old.Number) == 0 {
comment := strings.TrimSpace(sdata[off:])
edit.Body = &comment
- issue, _, err := client.Issues.Create(projectOwner, projectRepo, &edit)
+ issue, resp, err := client.Issues.Create(context.TODO(), projectOwner(project), projectRepo(project), &edit)
+ if resp != nil {
+ rate = &resp.Rate
+ }
if err != nil {
fmt.Fprintf(&errbuf, "error creating issue: %v\n", err)
- return nil, nil
+ return nil, rate, nil
}
- return issue, nil
+ return issue, rate, nil
}
if getInt(old.Number) == -1 {
// Asking to just sanity check the text parsing.
- return nil, nil
+ return nil, nil, nil
}
marker := "\nReported by "
@@ -178,9 +182,12 @@ func writeIssue(old *github.Issue, updated []byte, isBulk bool) (issue *github.I
var failed bool
var did []string
if comment != "" {
- _, _, err := client.Issues.CreateComment(projectOwner, projectRepo, getInt(old.Number), &github.IssueComment{
+ _, resp, err := client.Issues.CreateComment(context.TODO(), projectOwner(project), projectRepo(project), getInt(old.Number), &github.IssueComment{
Body: &comment,
})
+ if resp != nil {
+ rate = &resp.Rate
+ }
if err != nil {
fmt.Fprintf(&errbuf, "error saving comment: %v\n", err)
failed = true
@@ -190,7 +197,10 @@ func writeIssue(old *github.Issue, updated []byte, isBulk bool) (issue *github.I
}
if edit.Title != nil || edit.State != nil || edit.Assignee != nil || edit.Labels != nil || edit.Milestone != nil {
- _, _, err := client.Issues.Edit(projectOwner, projectRepo, getInt(old.Number), &edit)
+ _, resp, err := client.Issues.Edit(context.TODO(), projectOwner(project), projectRepo(project), getInt(old.Number), &edit)
+ if resp != nil {
+ rate = &resp.Rate
+ }
if err != nil {
fmt.Fprintf(&errbuf, "error changing metadata: %v\n", err)
failed = true
@@ -199,7 +209,10 @@ func writeIssue(old *github.Issue, updated []byte, isBulk bool) (issue *github.I
}
}
if len(addLabels) > 0 {
- _, _, err := client.Issues.AddLabelsToIssue(projectOwner, projectRepo, getInt(old.Number), addLabels)
+ _, resp, err := client.Issues.AddLabelsToIssue(context.TODO(), projectOwner(project), projectRepo(project), getInt(old.Number), addLabels)
+ if resp != nil {
+ rate = &resp.Rate
+ }
if err != nil {
fmt.Fprintf(&errbuf, "error adding labels: %v\n", err)
failed = true
@@ -213,7 +226,10 @@ func writeIssue(old *github.Issue, updated []byte, isBulk bool) (issue *github.I
}
if len(removeLabels) > 0 {
for _, label := range removeLabels {
- _, err := client.Issues.RemoveLabelForIssue(projectOwner, projectRepo, getInt(old.Number), label)
+ resp, err := client.Issues.RemoveLabelForIssue(context.TODO(), projectOwner(project), projectRepo(project), getInt(old.Number), label)
+ if resp != nil {
+ rate = &resp.Rate
+ }
if err != nil {
fmt.Fprintf(&errbuf, "error removing label %s: %v\n", label, err)
failed = true
@@ -290,12 +306,12 @@ func diffList2(line, field string, old []string) (added, removed []string) {
return
}
-func findMilestone(w io.Writer, name *string) *int {
+func findMilestone(w io.Writer, project string, name *string) *int {
if name == nil {
return nil
}
- all, err := loadMilestones()
+ all, err := loadMilestones(project)
if err != nil {
fmt.Fprintf(w, "Error loading milestone list: %v\n\tIgnoring milestone change.\n", err)
return nil
@@ -329,12 +345,12 @@ func readBulkIDs(text []byte) []int {
return ids
}
-func bulkEditStartFromText(content []byte) (base *github.Issue, original []byte, err error) {
+func bulkEditStartFromText(project string, content []byte) (base *github.Issue, original []byte, err error) {
ids := readBulkIDs(content)
if len(ids) == 0 {
return nil, nil, fmt.Errorf("found no issues in selection")
}
- issues, err := bulkReadIssuesCached(ids)
+ issues, err := bulkReadIssuesCached(project, ids)
if err != nil {
return nil, nil, err
}
@@ -349,14 +365,14 @@ func suffix(n int) string {
return "s"
}
-func bulkEditIssues(issues []*github.Issue) {
+func bulkEditIssues(project string, issues []*github.Issue) {
base, original := bulkEditStart(issues)
updated := editText(original)
if bytes.Equal(original, updated) {
log.Print("no changes made")
return
}
- ids, err := bulkWriteIssue(base, updated, func(s string) { log.Print(s) })
+ ids, err := bulkWriteIssue(project, base, updated, func(s string) { log.Print(s) })
if err != nil {
errText := strings.Replace(err.Error(), "\n", "\t\n", -1)
if len(ids) > 0 {
@@ -427,7 +443,7 @@ func commonLabels(x, y []github.Label) []github.Label {
return out
}
-func bulkWriteIssue(old *github.Issue, updated []byte, status func(string)) (ids []int, err error) {
+func bulkWriteIssue(project string, old *github.Issue, updated []byte, status func(string)) (ids []int, err error) {
i := bytes.Index(updated, []byte(bulkHeader))
if i < 0 {
return nil, fmt.Errorf("cannot find bulk edit issue list")
@@ -444,7 +460,8 @@ func bulkWriteIssue(old *github.Issue, updated []byte, status func(string)) (ids
// Try a write to issue -1, checking for formatting only.
old.Number = new(int)
*old.Number = -1
- if _, err := writeIssue(old, updated, true); err != nil {
+ _, rate, err := writeIssue(project, old, updated, true)
+ if err != nil {
return nil, err
}
@@ -462,19 +479,24 @@ func bulkWriteIssue(old *github.Issue, updated []byte, status func(string)) (ids
}
// Check rate limits here (in contrast to everywhere else in this program)
// to avoid needless failure halfway through the loop.
- for client.Rate().Limit > 0 && client.Rate().Remaining == 0 {
- delta := (client.Rate().Reset.Sub(time.Now())/time.Minute + 2) * time.Minute
+ for rate != nil && rate.Limit > 0 && rate.Remaining == 0 {
+ delta := (rate.Reset.Sub(time.Now())/time.Minute + 2) * time.Minute
if delta < 0 {
delta = 2 * time.Minute
}
status(fmt.Sprintf("updated %d/%d issues; pausing %d minutes to respect GitHub rate limit", index, len(ids), int(delta/time.Minute)))
time.Sleep(delta)
- if _, _, err := client.RateLimit(); err != nil {
+ limits, _, err := client.RateLimits(context.TODO())
+ if err != nil {
status(fmt.Sprintf("reading rate limit: %v", err))
}
+ rate = nil
+ if limits != nil {
+ rate = limits.Core
+ }
}
*old.Number = number
- if _, err := writeIssue(old, updated, true); err != nil {
+ if _, rate, err = writeIssue(project, old, updated, true); err != nil {
status(fmt.Sprintf("writing #%d: %s", number, strings.Replace(err.Error(), "\n", "\n\t", -1)))
failed = true
}
@@ -485,3 +507,11 @@ func bulkWriteIssue(old *github.Issue, updated []byte, status func(string)) (ids
}
return ids, nil
}
+
+func projectOwner(project string) string {
+ return project[:strings.Index(project, "/")]
+}
+
+func projectRepo(project string) string {
+ return project[strings.Index(project, "/")+1:]
+}
diff --git a/issue/issue.go b/issue/issue.go
@@ -231,6 +231,7 @@ package main // import "rsc.io/github/issue"
import (
"bytes"
+ "context"
"encoding/json"
"flag"
"fmt"
@@ -251,14 +252,13 @@ import (
)
var (
- acmeFlag = flag.Bool("a", false, "open in new acme window")
- editFlag = flag.Bool("e", false, "edit in system editor")
- jsonFlag = flag.Bool("json", false, "write JSON output")
- project = flag.String("p", "golang/go", "GitHub owner/repo name")
- rawFlag = flag.Bool("raw", false, "do no processing of markdown")
- tokenFile = flag.String("token", "", "read GitHub token personal access token from `file` (default $HOME/.github-issue-token)")
- projectOwner = ""
- projectRepo = ""
+ acmeFlag = flag.Bool("a", false, "open in new acme window")
+ editFlag = flag.Bool("e", false, "edit in system editor")
+ jsonFlag = flag.Bool("json", false, "write JSON output")
+ project = flag.String("p", "golang/go", "GitHub owner/repo name")
+ rawFlag = flag.Bool("raw", false, "do no processing of markdown")
+ tokenFile = flag.String("token", "", "read GitHub token personal access token from `file` (default $HOME/.github-issue-token)")
+ logHTTP = flag.Bool("loghttp", false, "log http requests")
)
func usage() {
@@ -288,12 +288,14 @@ func main() {
log.Fatal("cannot use -e with -acme")
}
+ if *logHTTP {
+ http.DefaultTransport = newLogger(http.DefaultTransport)
+ }
+
f := strings.Split(*project, "/")
if len(f) != 2 {
log.Fatal("invalid form for -p argument: must be owner/repo, like golang/go")
}
- projectOwner = f[0]
- projectRepo = f[1]
loadAuth()
@@ -304,7 +306,7 @@ func main() {
q := strings.Join(flag.Args(), " ")
if *editFlag && q == "new" {
- editIssue([]byte(createTemplate), new(github.Issue))
+ editIssue(*project, []byte(createTemplate), new(github.Issue))
return
}
@@ -312,21 +314,21 @@ func main() {
if n != 0 {
if *editFlag {
var buf bytes.Buffer
- issue, err := showIssue(&buf, n)
+ issue, err := showIssue(&buf, *project, n)
if err != nil {
log.Fatal(err)
}
- editIssue(buf.Bytes(), issue)
+ editIssue(*project, buf.Bytes(), issue)
return
}
- if _, err := showIssue(os.Stdout, n); err != nil {
+ if _, err := showIssue(os.Stdout, *project, n); err != nil {
log.Fatal(err)
}
return
}
if *editFlag {
- all, err := searchIssues(q)
+ all, err := searchIssues(*project, q)
if err != nil {
log.Fatal(err)
}
@@ -334,29 +336,29 @@ func main() {
log.Fatal("no issues matched search")
}
sort.Sort(issuesByTitle(all))
- bulkEditIssues(all)
+ bulkEditIssues(*project, all)
return
}
- if err := showQuery(os.Stdout, q); err != nil {
+ if err := showQuery(os.Stdout, *project, q); err != nil {
log.Fatal(err)
}
}
-func showIssue(w io.Writer, n int) (*github.Issue, error) {
- issue, _, err := client.Issues.Get(projectOwner, projectRepo, n)
+func showIssue(w io.Writer, project string, n int) (*github.Issue, error) {
+ issue, _, err := client.Issues.Get(context.TODO(), projectOwner(project), projectRepo(project), n)
if err != nil {
return nil, err
}
- updateIssueCache(issue)
- return issue, printIssue(w, issue)
+ updateIssueCache(project, issue)
+ return issue, printIssue(w, project, issue)
}
const timeFormat = "2006-01-02 15:04:05"
-func printIssue(w io.Writer, issue *github.Issue) error {
+func printIssue(w io.Writer, project string, issue *github.Issue) error {
if *jsonFlag {
- showJSONIssue(w, issue)
+ showJSONIssue(w, project, issue)
return nil
}
@@ -368,7 +370,7 @@ func printIssue(w io.Writer, issue *github.Issue) error {
}
fmt.Fprintf(w, "Labels: %s\n", strings.Join(getLabelNames(issue.Labels), " "))
fmt.Fprintf(w, "Milestone: %s\n", getMilestoneTitle(issue.Milestone))
- fmt.Fprintf(w, "URL: https://github.com/%s/%s/issues/%d\n", projectOwner, projectRepo, getInt(issue.Number))
+ fmt.Fprintf(w, "URL: https://github.com/%s/%s/issues/%d\n", projectOwner(project), projectRepo(project), getInt(issue.Number))
fmt.Fprintf(w, "\nReported by %s (%s)\n", getUserLogin(issue.User), getTime(issue.CreatedAt).Format(timeFormat))
if issue.Body != nil {
@@ -385,7 +387,7 @@ func printIssue(w io.Writer, issue *github.Issue) error {
var output []string
for page := 1; ; {
- list, resp, err := client.Issues.ListComments(projectOwner, projectRepo, getInt(issue.Number), &github.IssueListCommentsOptions{
+ list, resp, err := client.Issues.ListComments(context.TODO(), projectOwner(project), projectRepo(project), getInt(issue.Number), &github.IssueListCommentsOptions{
ListOptions: github.ListOptions{
Page: page,
PerPage: 100,
@@ -418,7 +420,7 @@ func printIssue(w io.Writer, issue *github.Issue) error {
}
for page := 1; ; {
- list, resp, err := client.Issues.ListIssueEvents(projectOwner, projectRepo, getInt(issue.Number), &github.ListOptions{
+ list, resp, err := client.Issues.ListIssueEvents(context.TODO(), projectOwner(project), projectRepo(project), getInt(issue.Number), &github.ListOptions{
Page: page,
PerPage: 100,
})
@@ -441,7 +443,7 @@ func printIssue(w io.Writer, issue *github.Issue) error {
}
fmt.Fprintf(w, "\n* %s %s%s (%s)\n", getUserLogin(ev.Actor), event, id, getTime(ev.CreatedAt).Format(timeFormat))
if id != "" {
- commit, _, err := client.Git.GetCommit(projectOwner, projectRepo, *ev.CommitID)
+ commit, _, err := client.Git.GetCommit(context.TODO(), projectOwner(project), projectRepo(project), *ev.CommitID)
if err == nil {
fmt.Fprintf(w, "\n\tAuthor: %s <%s> %s\n\tCommitter: %s <%s> %s\n\n\t%s\n",
getString(commit.Author.Name), getString(commit.Author.Email), getTime(commit.Author.Date).Format(timeFormat),
@@ -483,14 +485,14 @@ func printIssue(w io.Writer, issue *github.Issue) error {
return nil
}
-func showQuery(w io.Writer, q string) error {
- all, err := searchIssues(q)
+func showQuery(w io.Writer, project, q string) error {
+ all, err := searchIssues(project, q)
if err != nil {
return err
}
sort.Sort(issuesByTitle(all))
if *jsonFlag {
- showJSONList(all)
+ showJSONList(project, all)
return nil
}
for _, issue := range all {
@@ -510,22 +512,22 @@ func (x issuesByTitle) Less(i, j int) bool {
return getInt(x[i].Number) < getInt(x[j].Number)
}
-func searchIssues(q string) ([]*github.Issue, error) {
- if opt, ok := queryToListOptions(q); ok {
- return listRepoIssues(opt)
+func searchIssues(project, q string) ([]*github.Issue, error) {
+ if opt, ok := queryToListOptions(project, q); ok {
+ return listRepoIssues(project, opt)
}
var all []*github.Issue
for page := 1; ; {
// TODO(rsc): Rethink excluding pull requests.
- x, resp, err := client.Search.Issues("type:issue state:open repo:"+*project+" "+q, &github.SearchOptions{
+ x, resp, err := client.Search.Issues(context.TODO(), "type:issue state:open repo:"+project+" "+q, &github.SearchOptions{
ListOptions: github.ListOptions{
Page: page,
PerPage: 100,
},
})
for i := range x.Issues {
- updateIssueCache(&x.Issues[i])
+ updateIssueCache(project, &x.Issues[i])
all = append(all, &x.Issues[i])
}
if err != nil {
@@ -539,7 +541,7 @@ func searchIssues(q string) ([]*github.Issue, error) {
return all, nil
}
-func queryToListOptions(q string) (opt github.IssueListByRepoOptions, ok bool) {
+func queryToListOptions(project, q string) (opt github.IssueListByRepoOptions, ok bool) {
if strings.ContainsAny(q, `"'`) {
return
}
@@ -556,7 +558,7 @@ func queryToListOptions(q string) (opt github.IssueListByRepoOptions, ok bool) {
if opt.Milestone != "" || val == "" {
return
}
- id := findMilestone(ioutil.Discard, &val)
+ id := findMilestone(ioutil.Discard, project, &val)
if id == nil {
return
}
@@ -612,7 +614,7 @@ func queryToListOptions(q string) (opt github.IssueListByRepoOptions, ok bool) {
return opt, true
}
-func listRepoIssues(opt github.IssueListByRepoOptions) ([]*github.Issue, error) {
+func listRepoIssues(project string, opt github.IssueListByRepoOptions) ([]*github.Issue, error) {
var all []*github.Issue
for page := 1; ; {
xopt := opt
@@ -620,9 +622,9 @@ func listRepoIssues(opt github.IssueListByRepoOptions) ([]*github.Issue, error)
Page: page,
PerPage: 100,
}
- issues, resp, err := client.Issues.ListByRepo(projectOwner, projectRepo, &xopt)
+ issues, resp, err := client.Issues.ListByRepo(context.TODO(), projectOwner(project), projectRepo(project), &xopt)
for i := range issues {
- updateIssueCache(issues[i])
+ updateIssueCache(project, issues[i])
all = append(all, issues[i])
}
if err != nil {
@@ -645,9 +647,9 @@ func listRepoIssues(opt github.IssueListByRepoOptions) ([]*github.Issue, error)
return save, nil
}
-func loadMilestones() ([]*github.Milestone, error) {
+func loadMilestones(project string) ([]*github.Milestone, error) {
// NOTE(rsc): There appears to be no paging possible.
- all, _, err := client.Issues.ListMilestones(projectOwner, projectRepo, &github.MilestoneListOptions{
+ all, _, err := client.Issues.ListMilestones(context.TODO(), projectOwner(project), projectRepo(project), &github.MilestoneListOptions{
State: "open",
})
if err != nil {
@@ -770,41 +772,46 @@ func getLabelNames(x []github.Label) []string {
return out
}
+type projectAndNumber struct {
+ project string
+ number int
+}
+
var issueCache struct {
sync.Mutex
- m map[int]*github.Issue
+ m map[projectAndNumber]*github.Issue
}
-func updateIssueCache(issue *github.Issue) {
+func updateIssueCache(project string, issue *github.Issue) {
n := getInt(issue.Number)
if n == 0 {
return
}
issueCache.Lock()
if issueCache.m == nil {
- issueCache.m = make(map[int]*github.Issue)
+ issueCache.m = make(map[projectAndNumber]*github.Issue)
}
- issueCache.m[n] = issue
+ issueCache.m[projectAndNumber{project, n}] = issue
issueCache.Unlock()
}
-func bulkReadIssuesCached(ids []int) ([]*github.Issue, error) {
+func bulkReadIssuesCached(project string, ids []int) ([]*github.Issue, error) {
var all []*github.Issue
issueCache.Lock()
for _, id := range ids {
- all = append(all, issueCache.m[id])
+ all = append(all, issueCache.m[projectAndNumber{project, id}])
}
issueCache.Unlock()
var errbuf bytes.Buffer
for i, id := range ids {
if all[i] == nil {
- issue, _, err := client.Issues.Get(projectOwner, projectRepo, id)
+ issue, _, err := client.Issues.Get(context.TODO(), projectOwner(project), projectRepo(project), id)
if err != nil {
fmt.Fprintf(&errbuf, "reading #%d: %v\n", id, err)
continue
}
- updateIssueCache(issue)
+ updateIssueCache(project, issue)
all[i] = issue
}
}
@@ -840,8 +847,8 @@ type Comment struct {
Text string
}
-func showJSONIssue(w io.Writer, issue *github.Issue) {
- data, err := json.MarshalIndent(toJSONWithComments(issue), "", "\t")
+func showJSONIssue(w io.Writer, project string, issue *github.Issue) {
+ data, err := json.MarshalIndent(toJSONWithComments(project, issue), "", "\t")
if err != nil {
log.Fatal(err)
}
@@ -849,10 +856,10 @@ func showJSONIssue(w io.Writer, issue *github.Issue) {
w.Write(data)
}
-func showJSONList(all []*github.Issue) {
+func showJSONList(project string, all []*github.Issue) {
j := []*Issue{} // non-nil for json
for _, issue := range all {
- j = append(j, toJSON(issue))
+ j = append(j, toJSON(project, issue))
}
data, err := json.MarshalIndent(j, "", "\t")
if err != nil {
@@ -862,17 +869,17 @@ func showJSONList(all []*github.Issue) {
os.Stdout.Write(data)
}
-func toJSON(issue *github.Issue) *Issue {
+func toJSON(project string, issue *github.Issue) *Issue {
j := &Issue{
Number: getInt(issue.Number),
- Ref: fmt.Sprintf("%s/%s#%d\n", projectOwner, projectRepo, getInt(issue.Number)),
+ Ref: fmt.Sprintf("%s/%s#%d\n", projectOwner(project), projectRepo(project), getInt(issue.Number)),
Title: getString(issue.Title),
State: getString(issue.State),
Assignee: getUserLogin(issue.Assignee),
Closed: getTime(issue.ClosedAt),
Labels: getLabelNames(issue.Labels),
Milestone: getMilestoneTitle(issue.Milestone),
- URL: fmt.Sprintf("https://github.com/%s/%s/issues/%d\n", projectOwner, projectRepo, getInt(issue.Number)),
+ URL: fmt.Sprintf("https://github.com/%s/%s/issues/%d\n", projectOwner(project), projectRepo(project), getInt(issue.Number)),
Reporter: getUserLogin(issue.User),
Created: getTime(issue.CreatedAt),
Text: getString(issue.Body),
@@ -884,10 +891,10 @@ func toJSON(issue *github.Issue) *Issue {
return j
}
-func toJSONWithComments(issue *github.Issue) *Issue {
- j := toJSON(issue)
+func toJSONWithComments(project string, issue *github.Issue) *Issue {
+ j := toJSON(project, issue)
for page := 1; ; {
- list, resp, err := client.Issues.ListComments(projectOwner, projectRepo, getInt(issue.Number), &github.IssueListCommentsOptions{
+ list, resp, err := client.Issues.ListComments(context.TODO(), projectOwner(project), projectRepo(project), getInt(issue.Number), &github.IssueListCommentsOptions{
ListOptions: github.ListOptions{
Page: page,
PerPage: 100,
@@ -910,3 +917,54 @@ func toJSONWithComments(issue *github.Issue) *Issue {
}
return j
}
+
+func newLogger(t http.RoundTripper) http.RoundTripper {
+ return &loggingTransport{transport: t}
+}
+
+type loggingTransport struct {
+ transport http.RoundTripper
+ mu sync.Mutex
+ active []byte
+}
+
+func (t *loggingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
+ t.mu.Lock()
+ index := len(t.active)
+ start := time.Now()
+ fmt.Fprintf(os.Stderr, "HTTP: %s %s+ %s\n", timeFormat1(start), t.active, r.URL)
+ t.active = append(t.active, '|')
+ t.mu.Unlock()
+
+ resp, err := t.transport.RoundTrip(r)
+
+ last := r.URL.Path
+ if i := strings.LastIndex(last, "/"); i >= 0 {
+ last = last[i:]
+ }
+ display := last
+ if resp != nil {
+ display += " " + resp.Status
+ }
+ if err != nil {
+ display += " error: " + err.Error()
+ }
+ now := time.Now()
+
+ t.mu.Lock()
+ t.active[index] = '-'
+ fmt.Fprintf(os.Stderr, "HTTP: %s %s %s (%.3fs)\n", timeFormat1(now), t.active, display, now.Sub(start).Seconds())
+ t.active[index] = ' '
+ n := len(t.active)
+ for n%4 == 0 && n >= 4 && t.active[n-1] == ' ' && t.active[n-2] == ' ' && t.active[n-3] == ' ' && t.active[n-4] == ' ' {
+ t.active = t.active[:n-4]
+ n -= 4
+ }
+ t.mu.Unlock()
+
+ return resp, err
+}
+
+func timeFormat1(t time.Time) string {
+ return t.Format("15:04:05.000")
+}