@@ -77,6 +77,7 @@ import (
77
77
"net/url"
78
78
"os"
79
79
"os/exec"
80
+ "path"
80
81
"path/filepath"
81
82
"regexp"
82
83
"runtime"
@@ -86,10 +87,12 @@ import (
86
87
"time"
87
88
"unicode"
88
89
90
+ "golang.org/x/mod/semver"
89
91
"golang.org/x/sys/unix"
90
92
"golang.org/x/telemetry"
91
93
"golang.org/x/tools/gopls/internal/util/browser"
92
94
"golang.org/x/tools/gopls/internal/util/moremaps"
95
+ "golang.org/x/tools/gopls/internal/util/morestrings"
93
96
)
94
97
95
98
// flags
@@ -243,8 +246,15 @@ func main() {
243
246
if issue , ok := claimedBy [id ]; ok {
244
247
// existing issue, already updated above, just store
245
248
// the summary.
249
+ state := issue .State
250
+ if issue .State == "closed" && issue .StateReason == "completed" {
251
+ state = "completed"
252
+ }
246
253
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
+ }
248
258
existingIssues [summary ] += total
249
259
} else {
250
260
// new issue, need to create GitHub issue and store
@@ -266,7 +276,7 @@ func main() {
266
276
for _ , summary := range keys {
267
277
count := issues [summary ]
268
278
// 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]" ) ) {
270
280
// ESC + "[" + n + "m" => change color to n
271
281
// (37 = white, 0 = default)
272
282
summary = "\x1B [37m" + summary + "\x1B [0m"
@@ -590,8 +600,14 @@ func updateIssues(cli *githubClient, issues []*Issue, stacks map[string]map[Info
590
600
body += "\n Dups:"
591
601
}
592
602
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" ,
595
611
issue .Number , err )
596
612
continue
597
613
}
@@ -600,6 +616,50 @@ func updateIssues(cli *githubClient, issues []*Issue, stacks map[string]map[Info
600
616
}
601
617
}
602
618
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
+
603
663
// stackID returns a 32-bit identifier for a stack
604
664
// suitable for use in GitHub issue titles.
605
665
func stackID (stack string ) string {
@@ -819,16 +879,27 @@ type githubClient struct {
819
879
changes []any // slice of (addIssueComment | updateIssueBody)
820
880
}
821
881
882
+ func (cli * githubClient ) takeChanges () []any {
883
+ r := cli .changes
884
+ cli .changes = nil
885
+ return r
886
+ }
887
+
822
888
// addIssueComment is a change for creating a comment on an issue.
823
889
type addIssueComment struct {
824
890
number int
825
891
comment string
826
892
}
827
893
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"
832
903
}
833
904
834
905
// -- GitHub search --
@@ -888,24 +959,19 @@ func (cli *githubClient) searchIssues(label string) ([]*Issue, error) {
888
959
return results , nil
889
960
}
890
961
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 {
893
964
if cli .divertChanges {
894
- cli .changes = append (cli .changes , updateIssueBody { number , body } )
965
+ cli .changes = append (cli .changes , update )
895
966
return nil
896
967
}
897
968
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 )
904
970
if err != nil {
905
971
return err
906
972
}
907
973
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 )
909
975
if err := cli .requestChange ("PATCH" , url , data , http .StatusOK ); err != nil {
910
976
return fmt .Errorf ("updating issue: %v" , err )
911
977
}
@@ -963,13 +1029,15 @@ func (cli *githubClient) requestChange(method, url string, data []byte, wantStat
963
1029
// See https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues.
964
1030
965
1031
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
973
1041
974
1042
// Set by readIssues.
975
1043
predicate func (string ) bool // matching predicate over stack text
@@ -983,6 +1051,10 @@ type User struct {
983
1051
HTMLURL string `json:"html_url"`
984
1052
}
985
1053
1054
+ type Milestone struct {
1055
+ Title string
1056
+ }
1057
+
986
1058
// -- pclntab --
987
1059
988
1060
type FileLine struct {
0 commit comments