@@ -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 += "\n Dups:"
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.
605665func 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.
823889type 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
9651031type 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
9881060type FileLine struct {
0 commit comments