Skip to content

Commit 4f1e910

Browse files
committed
internal/telemetry/cmd/stacks: reopen issues
Re-open an issue if it was closed as fixed, but we encounter a new stack in a later version. For golang/go#71045. Change-Id: If6a4fe4091588e42b6f6c47e8705313352dc295e Reviewed-on: https://go-review.googlesource.com/c/tools/+/644115 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 114ac82 commit 4f1e910

File tree

4 files changed

+290
-65
lines changed

4 files changed

+290
-65
lines changed

gopls/internal/golang/assembly.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"strings"
2222

2323
"golang.org/x/tools/gopls/internal/cache"
24+
"golang.org/x/tools/gopls/internal/util/morestrings"
2425
)
2526

2627
// AssemblyHTML returns an HTML document containing an assembly listing of the selected function.
@@ -103,7 +104,7 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pack
103104
// Skip filenames of the form "<foo>".
104105
if parts := insnRx.FindStringSubmatch(line); parts != nil {
105106
link := " " // if unknown
106-
if file, linenum, ok := cutLast(parts[2], ":"); ok && !strings.HasPrefix(file, "<") {
107+
if file, linenum, ok := morestrings.CutLast(parts[2], ":"); ok && !strings.HasPrefix(file, "<") {
107108
if linenum, err := strconv.Atoi(linenum); err == nil {
108109
text := fmt.Sprintf("L%04d", linenum)
109110
link = sourceLink(text, web.SrcURL(file, linenum, 1))
@@ -117,11 +118,3 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pack
117118
}
118119
return buf.Bytes(), nil
119120
}
120-
121-
// cutLast is the "last" analogue of [strings.Cut].
122-
func cutLast(s, sep string) (before, after string, ok bool) {
123-
if i := strings.LastIndex(s, sep); i >= 0 {
124-
return s[:i], s[i+len(sep):], true
125-
}
126-
return s, "", false
127-
}

gopls/internal/telemetry/cmd/stacks/stacks.go

Lines changed: 97 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import (
7777
"net/url"
7878
"os"
7979
"os/exec"
80+
"path"
8081
"path/filepath"
8182
"regexp"
8283
"runtime"
@@ -86,10 +87,12 @@ import (
8687
"time"
8788
"unicode"
8889

90+
"golang.org/x/mod/semver"
8991
"golang.org/x/sys/unix"
9092
"golang.org/x/telemetry"
9193
"golang.org/x/tools/gopls/internal/util/browser"
9294
"golang.org/x/tools/gopls/internal/util/moremaps"
95+
"golang.org/x/tools/gopls/internal/util/morestrings"
9396
)
9497

9598
// flags
@@ -243,8 +246,15 @@ func main() {
243246
if issue, ok := claimedBy[id]; ok {
244247
// existing issue, already updated above, just store
245248
// the summary.
249+
state := issue.State
250+
if issue.State == "closed" && issue.StateReason == "completed" {
251+
state = "completed"
252+
}
246253
summary := fmt.Sprintf("#%d: %s [%s]",
247-
issue.Number, issue.Title, issue.State)
254+
issue.Number, issue.Title, state)
255+
if state == "completed" && issue.Milestone != nil {
256+
summary += " milestone " + strings.TrimPrefix(issue.Milestone.Title, "gopls/")
257+
}
248258
existingIssues[summary] += total
249259
} else {
250260
// new issue, need to create GitHub issue and store
@@ -266,7 +276,7 @@ func main() {
266276
for _, summary := range keys {
267277
count := issues[summary]
268278
// Show closed issues in "white".
269-
if isTerminal(os.Stdout) && strings.Contains(summary, "[closed]") {
279+
if isTerminal(os.Stdout) && (strings.Contains(summary, "[closed]") || strings.Contains(summary, "[completed]")) {
270280
// ESC + "[" + n + "m" => change color to n
271281
// (37 = white, 0 = default)
272282
summary = "\x1B[37m" + summary + "\x1B[0m"
@@ -590,8 +600,14 @@ func updateIssues(cli *githubClient, issues []*Issue, stacks map[string]map[Info
590600
body += "\nDups:"
591601
}
592602
body += " " + strings.Join(newStackIDs, " ")
593-
if err := cli.updateIssueBody(issue.Number, body); err != nil {
594-
log.Printf("added comment to issue #%d but failed to update body: %v",
603+
604+
update := updateIssue{number: issue.Number, Body: body}
605+
if shouldReopen(issue, stacks) {
606+
update.State = "open"
607+
update.StateReason = "reopened"
608+
}
609+
if err := cli.updateIssue(update); err != nil {
610+
log.Printf("added comment to issue #%d but failed to update: %v",
595611
issue.Number, err)
596612
continue
597613
}
@@ -600,6 +616,50 @@ func updateIssues(cli *githubClient, issues []*Issue, stacks map[string]map[Info
600616
}
601617
}
602618

619+
// An issue should be re-opened if it was closed as fixed, and at least one of the
620+
// new stacks happened since the version containing the fix.
621+
func shouldReopen(issue *Issue, stacks map[string]map[Info]int64) bool {
622+
if !issue.isFixed() {
623+
return false
624+
}
625+
issueProgram, issueVersion, ok := parseMilestone(issue.Milestone)
626+
if !ok {
627+
return false
628+
}
629+
// TODO(jba?): handle other programs
630+
if issueProgram != "gopls" {
631+
return false
632+
}
633+
for _, stack := range issue.newStacks {
634+
for info := range stacks[stack] {
635+
if path.Base(info.Program) == issueProgram && semver.Compare(info.ProgramVersion, issueVersion) >= 0 {
636+
log.Printf("reopening issue #%d: purportedly fixed in %s@%s, but found a new stack from version %s",
637+
issue.Number, issueProgram, issueVersion, info.ProgramVersion)
638+
return true
639+
}
640+
}
641+
}
642+
return false
643+
}
644+
645+
// An issue is fixed if it was closed because it was completed.
646+
func (i *Issue) isFixed() bool {
647+
return i.State == "closed" && i.StateReason == "completed"
648+
}
649+
650+
// parseMilestone parses a the title of a GitHub milestone that is in the format
651+
// PROGRAM/VERSION. For example, "gopls/v0.17.0".
652+
func parseMilestone(m *Milestone) (program, version string, ok bool) {
653+
if m == nil {
654+
return "", "", false
655+
}
656+
program, version, ok = morestrings.CutLast(m.Title, "/")
657+
if !ok || program == "" || version == "" || version[0] != 'v' {
658+
return "", "", false
659+
}
660+
return program, version, true
661+
}
662+
603663
// stackID returns a 32-bit identifier for a stack
604664
// suitable for use in GitHub issue titles.
605665
func stackID(stack string) string {
@@ -819,16 +879,27 @@ type githubClient struct {
819879
changes []any // slice of (addIssueComment | updateIssueBody)
820880
}
821881

882+
func (cli *githubClient) takeChanges() []any {
883+
r := cli.changes
884+
cli.changes = nil
885+
return r
886+
}
887+
822888
// addIssueComment is a change for creating a comment on an issue.
823889
type addIssueComment struct {
824890
number int
825891
comment string
826892
}
827893

828-
// updateIssueBody is a change for modifying an existing issue's body.
829-
type updateIssueBody struct {
830-
number int
831-
body string
894+
// updateIssue is a change for modifying an existing issue.
895+
// It includes the issue number and the fields that can be updated on a GitHub issue.
896+
// A JSON-marshaled updateIssue can be used as the body of the update request sent to GitHub.
897+
// See https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#update-an-issue.
898+
type updateIssue struct {
899+
number int // issue number; must be unexported
900+
Body string `json:"body,omitempty"`
901+
State string `json:"state,omitempty"` // "open" or "closed"
902+
StateReason string `json:"state_reason,omitempty"` // "completed", "not_planned", "reopened"
832903
}
833904

834905
// -- GitHub search --
@@ -888,24 +959,19 @@ func (cli *githubClient) searchIssues(label string) ([]*Issue, error) {
888959
return results, nil
889960
}
890961

891-
// updateIssueBody updates the body of the numbered issue.
892-
func (cli *githubClient) updateIssueBody(number int, body string) error {
962+
// updateIssue updates the numbered issue.
963+
func (cli *githubClient) updateIssue(update updateIssue) error {
893964
if cli.divertChanges {
894-
cli.changes = append(cli.changes, updateIssueBody{number, body})
965+
cli.changes = append(cli.changes, update)
895966
return nil
896967
}
897968

898-
// https://docs.github.com/en/rest/issues/comments#update-an-issue
899-
var payload struct {
900-
Body string `json:"body"`
901-
}
902-
payload.Body = body
903-
data, err := json.Marshal(payload)
969+
data, err := json.Marshal(update)
904970
if err != nil {
905971
return err
906972
}
907973

908-
url := fmt.Sprintf("https://api.github.com/repos/golang/go/issues/%d", number)
974+
url := fmt.Sprintf("https://api.github.com/repos/golang/go/issues/%d", update.number)
909975
if err := cli.requestChange("PATCH", url, data, http.StatusOK); err != nil {
910976
return fmt.Errorf("updating issue: %v", err)
911977
}
@@ -963,13 +1029,15 @@ func (cli *githubClient) requestChange(method, url string, data []byte, wantStat
9631029
// See https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues.
9641030

9651031
type Issue struct {
966-
Number int
967-
HTMLURL string `json:"html_url"`
968-
Title string
969-
State string
970-
User *User
971-
CreatedAt time.Time `json:"created_at"`
972-
Body string // in Markdown format
1032+
Number int
1033+
HTMLURL string `json:"html_url"`
1034+
Title string
1035+
State string
1036+
StateReason string `json:"state_reason"`
1037+
User *User
1038+
CreatedAt time.Time `json:"created_at"`
1039+
Body string // in Markdown format
1040+
Milestone *Milestone
9731041

9741042
// Set by readIssues.
9751043
predicate func(string) bool // matching predicate over stack text
@@ -983,6 +1051,10 @@ type User struct {
9831051
HTMLURL string `json:"html_url"`
9841052
}
9851053

1054+
type Milestone struct {
1055+
Title string
1056+
}
1057+
9861058
// -- pclntab --
9871059

9881060
type FileLine struct {

0 commit comments

Comments
 (0)