Skip to content

Commit 31d859d

Browse files
cmd/gopherbot: add auto-submit functionality
If a CL has been labeled with "Auto-Submit", is submittable according to Gerrit, has a positive TryBot-Result vote, and has no unresolved comments then submit the change. This requires adding a new gerrit.Client method for the CL submission endpoint. Updates golang/go#48021 Change-Id: I3d5dafd1ca25a3cac5a40d7e9a744ba12ab44cae Reviewed-on: https://go-review.googlesource.com/c/build/+/341212 Trust: Roland Shoemaker <[email protected]> Run-TryBot: Roland Shoemaker <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent 3ed4219 commit 31d859d

File tree

2 files changed

+163
-0
lines changed

2 files changed

+163
-0
lines changed

cmd/gopherbot/gopherbot.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ var tasks = []struct {
384384
// Gerrit tasks are applied to all projects by default.
385385
{"abandon scratch reviews", (*gopherbot).abandonScratchReviews},
386386
{"assign reviewers to CLs", (*gopherbot).assignReviewersToCLs},
387+
{"auto-submit CLs", (*gopherbot).autoSubmitCLs},
387388

388389
// Tasks that are specific to the golang/vscode-go repo.
389390
{"set vscode-go milestones", (*gopherbot).setVSCodeGoMilestones},
@@ -2255,6 +2256,122 @@ func (b *gopherbot) humanReviewersOnChange(ctx context.Context, change gerritCha
22552256
return ids, count >= minHumans
22562257
}
22572258

2259+
// autoSubmitCLs submits CLs which are labelled "Auto-Submit", are submittable according to Gerrit,
2260+
// have a positive TryBot-Result label, and have no unresolved comments.
2261+
//
2262+
// See golang.org/issue/48021.
2263+
func (b *gopherbot) autoSubmitCLs(ctx context.Context) error {
2264+
// We only run this task if it was explicitly requested via
2265+
// the --only-run flag.
2266+
if *onlyRun == "" {
2267+
return nil
2268+
}
2269+
2270+
return b.corpus.Gerrit().ForeachProjectUnsorted(func(gp *maintner.GerritProject) error {
2271+
if gp.Server() != "go.googlesource.com" {
2272+
return nil
2273+
}
2274+
return gp.ForeachOpenCL(func(cl *maintner.GerritCL) error {
2275+
gc := gerritChange{gp.Project(), cl.Number}
2276+
if b.deletedChanges[gc] {
2277+
return nil
2278+
}
2279+
2280+
// Break out early (before making Gerrit API calls) if the Auto-Submit label
2281+
// hasn't been used at all in this CL.
2282+
var autosubmitPresent bool
2283+
for _, meta := range cl.Metas {
2284+
if strings.Contains(meta.Commit.Msg, "\nLabel: Auto-Submit") {
2285+
autosubmitPresent = true
2286+
break
2287+
}
2288+
}
2289+
if !autosubmitPresent {
2290+
return nil
2291+
}
2292+
2293+
// Skip this CL if there aren't Auto-Submit+1 and TryBot-Result+1 labels.
2294+
changeInfo, err := b.gerrit.GetChange(ctx, fmt.Sprint(cl.Number), gerrit.QueryChangesOpt{Fields: []string{"LABELS", "SUBMITTABLE"}})
2295+
if err != nil {
2296+
if httpErr, ok := err.(*gerrit.HTTPError); ok && httpErr.Res.StatusCode == http.StatusNotFound {
2297+
b.deletedChanges[gc] = true
2298+
}
2299+
log.Printf("Could not retrieve change %q: %v", gc.ID(), err)
2300+
return nil
2301+
}
2302+
if !(changeInfo.Labels["Auto-Submit"].Approved != nil && changeInfo.Labels["TryBot-Result"].Approved != nil) {
2303+
return nil
2304+
}
2305+
// NOTE: we might be able to skip this as well, since the revision action
2306+
// check will also cover this...
2307+
if !changeInfo.Submittable {
2308+
return nil
2309+
}
2310+
2311+
// Skip this CL if there are any unresolved comment threads.
2312+
comments, err := b.gerrit.ListChangeComments(ctx, fmt.Sprint(cl.Number))
2313+
if err != nil {
2314+
return err
2315+
}
2316+
for _, commentSet := range comments {
2317+
sort.Slice(commentSet, func(i, j int) bool {
2318+
return commentSet[i].Updated.Time().Before(commentSet[j].Updated.Time())
2319+
})
2320+
threads := make(map[string]bool)
2321+
for _, c := range commentSet {
2322+
id := c.ID
2323+
if c.InReplyTo != "" {
2324+
id = c.InReplyTo
2325+
}
2326+
threads[id] = *c.Unresolved
2327+
}
2328+
for _, unresolved := range threads {
2329+
if unresolved {
2330+
return nil
2331+
}
2332+
}
2333+
}
2334+
2335+
// We need to check the mergeability, as well as the submitability,
2336+
// as the latter doesn't take into account merge conflicts, just
2337+
// if the change satisfies the project submit rules.
2338+
//
2339+
// NOTE: this may now be redundant, since the revision action check
2340+
// below will also inherently checks mergeability, since the change
2341+
// cannot actually be submitted if there is a merge conflict. We
2342+
// may be able to just skip this entirely.
2343+
mi, err := b.gerrit.GetMergeable(ctx, fmt.Sprint(cl.Number), "current")
2344+
if err != nil {
2345+
return err
2346+
}
2347+
if !mi.Mergeable || mi.CommitMerged {
2348+
return nil
2349+
}
2350+
2351+
ra, err := b.gerrit.GetRevisionActions(ctx, fmt.Sprint(cl.Number), "current")
2352+
if err != nil {
2353+
return err
2354+
}
2355+
if ra["submit"] == nil || !ra["submit"].Enabled {
2356+
return nil
2357+
}
2358+
2359+
if *dryRun {
2360+
log.Printf("[dry-run] would've submitted CL https://golang.org/cl/%d ...", cl.Number)
2361+
return nil
2362+
}
2363+
log.Printf("submitting CL https://golang.org/cl/%d ...", cl.Number)
2364+
2365+
// TODO: if maintner isn't fast enough (or is too fast) and it re-runs this
2366+
// before the submission is noticed, we may run this more than once. This
2367+
// could be handled with a local cache of "recently submitted" changes to
2368+
// be ignored.
2369+
_, err = b.gerrit.SubmitChange(ctx, fmt.Sprint(cl.Number))
2370+
return err
2371+
})
2372+
})
2373+
}
2374+
22582375
// reviewerRe extracts the reviewer's Gerrit ID from a line that looks like:
22592376
//
22602377
// Reviewer: Rebecca Stambler <16140@62eb7196-b449-3ce5-99f1-c037f21e1705>

gerrit/gerrit.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,9 @@ type LabelInfo struct {
333333
Optional bool `json:"optional"`
334334

335335
// Fields set by LABELS field option:
336+
Approved *AccountInfo `json:"approved"`
336337

338+
// Fields set by DETAILED_LABELS option:
337339
All []ApprovalInfo `json:"all"`
338340
}
339341

@@ -999,3 +1001,47 @@ func (c *Client) GetGroupMembers(ctx context.Context, groupID string) ([]Account
9991001
err := c.do(ctx, &ais, "GET", "/groups/"+groupID+"/members")
10001002
return ais, err
10011003
}
1004+
1005+
// SubmitChange submits the given change.
1006+
// For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-change
1007+
// The changeID is https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
1008+
func (c *Client) SubmitChange(ctx context.Context, changeID string) (ChangeInfo, error) {
1009+
var change ChangeInfo
1010+
err := c.do(ctx, &change, "POST", "/changes/"+changeID+"/submit")
1011+
return change, err
1012+
}
1013+
1014+
// MergeableInfo contains information about the mergeability of a change.
1015+
//
1016+
// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#mergeable-info.
1017+
type MergeableInfo struct {
1018+
SubmitType string `json:"submit_type"`
1019+
Strategy string `json:"strategy"`
1020+
Mergeable bool `json:"mergeable"`
1021+
CommitMerged bool `json:"commit_merged"`
1022+
}
1023+
1024+
// GetMergeable retrieves mergeability information for a change at a specific revision.
1025+
func (c *Client) GetMergeable(ctx context.Context, changeID, revision string) (MergeableInfo, error) {
1026+
var mergeable MergeableInfo
1027+
err := c.do(ctx, &mergeable, "GET", "/changes/"+changeID+"/revisions/"+revision+"/mergeable")
1028+
return mergeable, err
1029+
}
1030+
1031+
// ActionInfo contains information about actions a client can make to
1032+
// maniuplate a resource.
1033+
//
1034+
// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#action-info.
1035+
type ActionInfo struct {
1036+
Method string `json:"method"`
1037+
Label string `json:"label"`
1038+
Title string `json:"title"`
1039+
Enabled bool `json:"enabled"`
1040+
}
1041+
1042+
// GetRevisionActions retrieves revision actions.
1043+
func (c *Client) GetRevisionActions(ctx context.Context, changeID, revision string) (map[string]*ActionInfo, error) {
1044+
var actions map[string]*ActionInfo
1045+
err := c.do(ctx, &actions, "GET", "/changes/"+changeID+"/revisions/"+revision+"/actions")
1046+
return actions, err
1047+
}

0 commit comments

Comments
 (0)