diff options
-rw-r--r-- | frontend/frontend.go | 40 | ||||
-rw-r--r-- | gerrit/changeset.go | 55 | ||||
-rw-r--r-- | gerrit/client.go | 173 | ||||
-rw-r--r-- | gerrit/serie.go (renamed from submitqueue/serie.go) | 14 | ||||
-rw-r--r-- | gerrit/series.go (renamed from submitqueue/series.go) | 35 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 58 | ||||
-rw-r--r-- | main.go | 49 | ||||
-rw-r--r-- | public/serie.tmpl.html | 2 | ||||
-rw-r--r-- | public/series.tmpl.html | 16 | ||||
-rw-r--r-- | public/submit-queue.tmpl.html | 82 | ||||
-rw-r--r-- | submitqueue/result.go | 55 | ||||
-rw-r--r-- | submitqueue/runner.go | 207 | ||||
-rw-r--r-- | submitqueue/submitqueue.go | 231 |
14 files changed, 484 insertions, 535 deletions
diff --git a/frontend/frontend.go b/frontend/frontend.go index eef246ea380a..abb5a3db4f70 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -13,9 +13,9 @@ import ( "github.com/tweag/gerrit-queue/gerrit" _ "github.com/tweag/gerrit-queue/statik" // register static assets "github.com/tweag/gerrit-queue/submitqueue" -) -//TODO: log last update + "github.com/apex/log/handlers/memory" +) //loadTemplate loads a list of templates, relative to the statikFS root, and a FuncMap, and returns a template object func loadTemplate(templateNames []string, funcMap template.FuncMap) (*template.Template, error) { @@ -47,46 +47,48 @@ func loadTemplate(templateNames []string, funcMap template.FuncMap) (*template.T return tmpl, nil } -// MakeFrontend configures the router and returns a new Frontend struct -func MakeFrontend(runner *submitqueue.Runner) http.Handler { +// MakeFrontend returns a http.Handler +func MakeFrontend(memoryHandler *memory.Handler, gerritClient *gerrit.Client, runner *submitqueue.Runner) http.Handler { router := gin.Default() - router.GET("/submit-queue.json", func(c *gin.Context) { - submitQueue, _, _ := runner.GetState() - c.JSON(http.StatusOK, submitQueue) - }) + projectName := gerritClient.GetProjectName() + branchName := gerritClient.GetBranchName() router.GET("/", func(c *gin.Context) { - submitQueue, currentlyRunning, results := runner.GetState() + var wipSerie *gerrit.Serie = nil + HEAD := "" + currentlyRunning := runner.IsCurrentlyRunning() + + // don't trigger operations requiring a lock + if !currentlyRunning { + wipSerie = runner.GetWIPSerie() + HEAD = gerritClient.GetHEAD() + } funcMap := template.FuncMap{ - "isAutoSubmittable": func(serie *submitqueue.Serie) bool { - return submitQueue.IsAutoSubmittable(serie) - }, "changesetURL": func(changeset *gerrit.Changeset) string { - return submitQueue.GetChangesetURL(changeset) + return gerritClient.GetChangesetURL(changeset) }, } tmpl := template.Must(loadTemplate([]string{ "submit-queue.tmpl.html", - "series.tmpl.html", "serie.tmpl.html", "changeset.tmpl.html", }, funcMap)) tmpl.ExecuteTemplate(c.Writer, "submit-queue.tmpl.html", gin.H{ // Config - "projectName": submitQueue.ProjectName, - "branchName": submitQueue.BranchName, + "projectName": projectName, + "branchName": branchName, // State "currentlyRunning": currentlyRunning, - "series": submitQueue.Series, - "HEAD": submitQueue.HEAD, + "wipSerie": wipSerie, + "HEAD": HEAD, // History - "results": results, + "memory": memoryHandler, }) }) return router diff --git a/gerrit/changeset.go b/gerrit/changeset.go index 38a489ec7dd9..71310a712514 100644 --- a/gerrit/changeset.go +++ b/gerrit/changeset.go @@ -5,7 +5,7 @@ import ( "fmt" goGerrit "github.com/andygrunwald/go-gerrit" - log "github.com/sirupsen/logrus" + "github.com/apex/log" ) // Changeset represents a single changeset @@ -14,8 +14,8 @@ type Changeset struct { changeInfo *goGerrit.ChangeInfo ChangeID string Number int - IsVerified bool - IsCodeReviewed bool + Verified int + CodeReviewed int HashTags []string CommitID string ParentCommitIDs []string @@ -29,8 +29,8 @@ func MakeChangeset(changeInfo *goGerrit.ChangeInfo) *Changeset { changeInfo: changeInfo, ChangeID: changeInfo.ChangeID, Number: changeInfo.Number, - IsVerified: isVerified(changeInfo), - IsCodeReviewed: isCodeReviewed(changeInfo), + Verified: labelInfoToInt(changeInfo.Labels["Verified"]), + CodeReviewed: labelInfoToInt(changeInfo.Labels["Code-Review"]), HashTags: changeInfo.Hashtags, CommitID: changeInfo.CurrentRevision, // yes, this IS the commit ID. ParentCommitIDs: getParentCommitIDs(changeInfo), @@ -39,12 +39,6 @@ func MakeChangeset(changeInfo *goGerrit.ChangeInfo) *Changeset { } } -// MakeMockChangeset creates a mock changeset -// func MakeMockChangeset(isVerified, IsCodeReviewed bool, hashTags []string, commitID string, parentCommitIDs []string, ownerName, subject string) *Changeset { -// //TODO impl -// return nil -//} - // HasTag returns true if a Changeset has the given tag. func (c *Changeset) HasTag(tag string) bool { hashTags := c.HashTags @@ -56,6 +50,18 @@ func (c *Changeset) HasTag(tag string) bool { return false } +// IsVerified returns true if the changeset passed CI, +// that's when somebody left the Approved (+1) on the "Verified" label +func (c *Changeset) IsVerified() bool { + return c.Verified == 1 +} + +// IsCodeReviewed returns true if the changeset passed code review, +// that's when somebody left the Recommended (+2) on the "Code-Review" label +func (c *Changeset) IsCodeReviewed() bool { + return c.CodeReviewed == 2 +} + func (c *Changeset) String() string { var b bytes.Buffer b.WriteString("Changeset") @@ -76,18 +82,21 @@ func FilterChangesets(changesets []*Changeset, f func(*Changeset) bool) []*Chang return newChangesets } -// isVerified returns true if the code passed CI, -// that's when somebody left the Approved (+1) on the "Verified" label -func isVerified(changeInfo *goGerrit.ChangeInfo) bool { - labels := changeInfo.Labels - return labels["Verified"].Approved.AccountID != 0 -} - -// isCodeReviewed returns true if the code passed code review, -// that's when somebody left the Recommended (+2) on the "Code-Review" label -func isCodeReviewed(changeInfo *goGerrit.ChangeInfo) bool { - labels := changeInfo.Labels - return labels["Code-Review"].Recommended.AccountID != 0 +// labelInfoToInt converts a goGerrit.LabelInfo to -2…+2 int +func labelInfoToInt(labelInfo goGerrit.LabelInfo) int { + if labelInfo.Recommended.AccountID != 0 { + return 2 + } + if labelInfo.Approved.AccountID != 0 { + return 1 + } + if labelInfo.Disliked.AccountID != 0 { + return -1 + } + if labelInfo.Rejected.AccountID != 0 { + return -2 + } + return 0 } // getParentCommitIDs returns the parent commit IDs of the goGerrit.ChangeInfo diff --git a/gerrit/client.go b/gerrit/client.go index c65b5016c1ff..fd8a244151ab 100644 --- a/gerrit/client.go +++ b/gerrit/client.go @@ -1,35 +1,52 @@ package gerrit import ( + "fmt" + goGerrit "github.com/andygrunwald/go-gerrit" + "github.com/apex/log" "net/url" ) // passed to gerrit when retrieving changesets -var additionalFields = []string{"LABELS", "CURRENT_REVISION", "CURRENT_COMMIT", "DETAILED_ACCOUNTS"} +var additionalFields = []string{ + "LABELS", + "CURRENT_REVISION", + "CURRENT_COMMIT", + "DETAILED_ACCOUNTS", +} // IClient defines the gerrit.Client interface type IClient interface { - SearchChangesets(queryString string) (changesets []*Changeset, Error error) - GetHEAD(projectName string, branchName string) (string, error) - GetChangeset(changeID string) (*Changeset, error) + Refresh() error + GetHEAD() string + GetBaseURL() string + GetChangesetURL(changeset *Changeset) string SubmitChangeset(changeset *Changeset) (*Changeset, error) RebaseChangeset(changeset *Changeset, ref string) (*Changeset, error) RemoveTag(changeset *Changeset, tag string) (*Changeset, error) - GetBaseURL() string + ChangesetIsRebasedOnHEAD(changeset *Changeset) bool + SerieIsRebasedOnHEAD(serie *Serie) bool + FilterSeries(filter func(s *Serie) bool) []*Serie + FindSerie(filter func(s *Serie) bool) *Serie } var _ IClient = &Client{} // Client provides some ways to interact with a gerrit instance type Client struct { - client *goGerrit.Client - baseURL string + client *goGerrit.Client + logger *log.Logger + baseURL string + projectName string + branchName string + series []*Serie + head string } // NewClient initializes a new gerrit client -func NewClient(URL, username, password string) (*Client, error) { +func NewClient(logger *log.Logger, URL, username, password, projectName, branchName string) (*Client, error) { urlParsed, err := url.Parse(URL) if err != nil { return nil, err @@ -43,17 +60,58 @@ func NewClient(URL, username, password string) (*Client, error) { return &Client{ client: goGerritClient, baseURL: URL, + logger: logger, }, nil } -// SearchChangesets fetches a list of changesets matching a passed query string -func (gerrit *Client) SearchChangesets(queryString string) (changesets []*Changeset, Error error) { +// refreshHEAD queries the commit ID of the selected project and branch +func (c *Client) refreshHEAD() (string, error) { + branchInfo, _, err := c.client.Projects.GetBranch(c.projectName, c.branchName) + if err != nil { + return "", err + } + return branchInfo.Revision, nil +} + +// GetHEAD returns the internally stored HEAD +func (c *Client) GetHEAD() string { + return c.head +} + +// Refresh causes the client to refresh internal view of gerrit +func (c *Client) Refresh() error { + c.logger.Debug("refreshing from gerrit") + HEAD, err := c.refreshHEAD() + if err != nil { + return err + } + c.head = HEAD + + var queryString = fmt.Sprintf("status:open project:%s branch:%s", c.projectName, c.branchName) + c.logger.Debugf("fetching changesets: %s", queryString) + changesets, err := c.fetchChangesets(queryString) + if err != nil { + return err + } + + c.logger.Warnf("assembling series…") + series, err := AssembleSeries(changesets, c.logger) + if err != nil { + return err + } + series = SortSeries(series) + c.series = series + return nil +} + +// fetchChangesets fetches a list of changesets matching a passed query string +func (c *Client) fetchChangesets(queryString string) (changesets []*Changeset, Error error) { opt := &goGerrit.QueryChangeOptions{} opt.Query = []string{ queryString, } - opt.AdditionalFields = additionalFields //TODO: check DETAILED_ACCOUNTS is needed - changes, _, err := gerrit.client.Changes.QueryChanges(opt) + opt.AdditionalFields = additionalFields + changes, _, err := c.client.Changes.QueryChanges(opt) if err != nil { return nil, err } @@ -66,22 +124,13 @@ func (gerrit *Client) SearchChangesets(queryString string) (changesets []*Change return changesets, nil } -// GetHEAD returns the commit ID of a selected branch -func (gerrit *Client) GetHEAD(projectName string, branchName string) (string, error) { - branchInfo, _, err := gerrit.client.Projects.GetBranch(projectName, branchName) - if err != nil { - return "", err - } - return branchInfo.Revision, nil -} - -// GetChangeset downloads an existing Changeset from gerrit, by its ID +// fetchChangeset downloads an existing Changeset from gerrit, by its ID // Gerrit's API is a bit sparse, and only returns what you explicitly ask it // This is used to refresh an existing changeset with more data. -func (gerrit *Client) GetChangeset(changeID string) (*Changeset, error) { +func (c *Client) fetchChangeset(changeID string) (*Changeset, error) { opt := goGerrit.ChangeOptions{} opt.AdditionalFields = []string{"LABELS", "DETAILED_ACCOUNTS"} - changeInfo, _, err := gerrit.client.Changes.GetChange(changeID, &opt) + changeInfo, _, err := c.client.Changes.GetChange(changeID, &opt) if err != nil { return nil, err } @@ -89,28 +138,30 @@ func (gerrit *Client) GetChangeset(changeID string) (*Changeset, error) { } // SubmitChangeset submits a given changeset, and returns a changeset afterwards. -func (gerrit *Client) SubmitChangeset(changeset *Changeset) (*Changeset, error) { - changeInfo, _, err := gerrit.client.Changes.SubmitChange(changeset.ChangeID, &goGerrit.SubmitInput{}) +// TODO: update HEAD +func (c *Client) SubmitChangeset(changeset *Changeset) (*Changeset, error) { + changeInfo, _, err := c.client.Changes.SubmitChange(changeset.ChangeID, &goGerrit.SubmitInput{}) if err != nil { return nil, err } - return gerrit.GetChangeset(changeInfo.ChangeID) + return c.fetchChangeset(changeInfo.ChangeID) } // RebaseChangeset rebases a given changeset on top of a given ref -func (gerrit *Client) RebaseChangeset(changeset *Changeset, ref string) (*Changeset, error) { - changeInfo, _, err := gerrit.client.Changes.RebaseChange(changeset.ChangeID, &goGerrit.RebaseInput{ +// TODO: update HEAD +func (c *Client) RebaseChangeset(changeset *Changeset, ref string) (*Changeset, error) { + changeInfo, _, err := c.client.Changes.RebaseChange(changeset.ChangeID, &goGerrit.RebaseInput{ Base: ref, }) if err != nil { return changeset, err } - return gerrit.GetChangeset(changeInfo.ChangeID) + return c.fetchChangeset(changeInfo.ChangeID) } // RemoveTag removes the submit queue tag from a changeset and updates gerrit // we never add, that's something users should do in the GUI. -func (gerrit *Client) RemoveTag(changeset *Changeset, tag string) (*Changeset, error) { +func (c *Client) RemoveTag(changeset *Changeset, tag string) (*Changeset, error) { hashTags := changeset.HashTags newHashTags := []string{} for _, hashTag := range hashTags { @@ -118,12 +169,66 @@ func (gerrit *Client) RemoveTag(changeset *Changeset, tag string) (*Changeset, e newHashTags = append(newHashTags, hashTag) } } - // TODO: implement set hashtags api in go-gerrit and use here + // TODO: implement setting hashtags api in go-gerrit and use here // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#set-hashtags return changeset, nil } // GetBaseURL returns the gerrit base URL -func (gerrit *Client) GetBaseURL() string { - return gerrit.baseURL +func (c *Client) GetBaseURL() string { + return c.baseURL +} + +// GetProjectName returns the configured gerrit project name +func (c *Client) GetProjectName() string { + return c.projectName +} + +// GetBranchName returns the configured gerrit branch name +func (c *Client) GetBranchName() string { + return c.branchName +} + +// GetChangesetURL returns the URL to view a given changeset +func (c *Client) GetChangesetURL(changeset *Changeset) string { + return fmt.Sprintf("%s/c/%s/+/%d", c.GetBaseURL(), c.projectName, changeset.Number) +} + +// ChangesetIsRebasedOnHEAD returns true if the changeset is rebased on the current HEAD +func (c *Client) ChangesetIsRebasedOnHEAD(changeset *Changeset) bool { + if len(changeset.ParentCommitIDs) != 1 { + return false + } + return changeset.ParentCommitIDs[0] == c.head +} + +// SerieIsRebasedOnHEAD returns true if the whole series is rebased on the current HEAD +// this is already the case if the first changeset in the series is rebased on the current HEAD +func (c *Client) SerieIsRebasedOnHEAD(serie *Serie) bool { + // an empty serie should not exist + if len(serie.ChangeSets) == 0 { + return false + } + return c.ChangesetIsRebasedOnHEAD(serie.ChangeSets[0]) +} + +// FilterSeries returns a subset of all Series, passing the given filter function +func (c *Client) FilterSeries(filter func(s *Serie) bool) []*Serie { + matchedSeries := []*Serie{} + for _, serie := range c.series { + if filter(serie) { + matchedSeries = append(matchedSeries, serie) + } + } + return matchedSeries +} + +// FindSerie returns the first serie that matches the filter, or nil if none was found +func (c *Client) FindSerie(filter func(s *Serie) bool) *Serie { + for _, serie := range c.series { + if filter(serie) { + return serie + } + } + return nil } diff --git a/submitqueue/serie.go b/gerrit/serie.go index d4cd739cdfdd..788cf46f4ea6 100644 --- a/submitqueue/serie.go +++ b/gerrit/serie.go @@ -1,18 +1,16 @@ -package submitqueue +package gerrit import ( "fmt" "strings" - "github.com/tweag/gerrit-queue/gerrit" - - log "github.com/sirupsen/logrus" + "github.com/apex/log" ) // Serie represents a list of successive changesets with an unbroken parent -> child relation, // starting from the parent. type Serie struct { - ChangeSets []*gerrit.Changeset + ChangeSets []*Changeset } // GetParentCommitIDs returns the parent commit IDs @@ -33,9 +31,7 @@ func (s *Serie) GetLeafCommitID() (string, error) { // CheckIntegrity checks that the series contains a properly ordered and connected chain of commits func (s *Serie) CheckIntegrity() error { - logger := log.WithFields(log.Fields{ - "serie": s, - }) + logger := log.WithField("serie", s) // an empty serie is invalid if len(s.ChangeSets) == 0 { return fmt.Errorf("An empty serie is invalid") @@ -71,7 +67,7 @@ func (s *Serie) CheckIntegrity() error { // FilterAllChangesets applies a filter function on all of the changesets in the series. // returns true if it returns true for all changesets, false otherwise -func (s *Serie) FilterAllChangesets(f func(c *gerrit.Changeset) bool) bool { +func (s *Serie) FilterAllChangesets(f func(c *Changeset) bool) bool { for _, changeset := range s.ChangeSets { if f(changeset) == false { return false diff --git a/submitqueue/series.go b/gerrit/series.go index c607e3ec2135..06d49805d35d 100644 --- a/submitqueue/series.go +++ b/gerrit/series.go @@ -1,34 +1,33 @@ -package submitqueue +package gerrit import ( "sort" - "github.com/tweag/gerrit-queue/gerrit" - - "github.com/sirupsen/logrus" + "github.com/apex/log" ) // AssembleSeries consumes a list of `Changeset`, and groups them together to series // +// We initially put every Changeset in its own Serie +// // As we have no control over the order of the passed changesets, -// we maintain two lookup tables, -// mapLeafToSerie, which allows to lookup a serie by its leaf commit id, -// to append to an existing serie -// and mapParentToSeries, which allows to lookup all series having a certain parent commit id, -// to prepend to any of the existing series -// if we can't find anything, we create a new series -func AssembleSeries(changesets []*gerrit.Changeset, log *logrus.Logger) ([]*Serie, error) { +// we maintain a lookup table, mapLeafToSerie, +// which allows to lookup a serie by its leaf commit id +// We concat series in a fixpoint approach +// because both appending and prepending is much more complex. +// Concatenation moves changesets of the later changeset in the previous one +// in a cleanup phase, we remove orphaned series (those without any changesets inside) +// afterwards, we do an integrity check, just to be on the safe side. +func AssembleSeries(changesets []*Changeset, log *log.Logger) ([]*Serie, error) { series := make([]*Serie, 0) mapLeafToSerie := make(map[string]*Serie, 0) for _, changeset := range changesets { - logger := log.WithFields(logrus.Fields{ - "changeset": changeset.String(), - }) + logger := log.WithField("changeset", changeset.String()) logger.Debug("creating initial serie") serie := &Serie{ - ChangeSets: []*gerrit.Changeset{changeset}, + ChangeSets: []*Changeset{changeset}, } series = append(series, serie) mapLeafToSerie[changeset.CommitID] = serie @@ -72,7 +71,7 @@ func AssembleSeries(changesets []*gerrit.Changeset, log *logrus.Logger) ([]*Seri mapLeafToSerie[myLeafCommitID] = otherSerie // orphan our serie - serie.ChangeSets = []*gerrit.Changeset{} + serie.ChangeSets = []*Changeset{} // remove the orphaned serie from the lookup table delete(mapLeafToSerie, myLeafCommitID) @@ -90,9 +89,7 @@ func AssembleSeries(changesets []*gerrit.Changeset, log *logrus.Logger) ([]*Seri // Check integrity, just to be on the safe side. for _, serie := range series { - logger := log.WithFields(logrus.Fields{ - "serie": serie.String(), - }) + logger := log.WithField("serie", serie.String()) logger.Debugf("checking integrity") err := serie.CheckIntegrity() if err != nil { diff --git a/go.mod b/go.mod index c428ff3cb44f..c03e8c43edbd 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.12 require ( github.com/andygrunwald/go-gerrit v0.0.0-20190825170856-5959a9bf9ff8 + github.com/apex/log v1.1.1 github.com/gin-gonic/gin v1.4.0 github.com/google/go-querystring v1.0.0 // indirect github.com/rakyll/statik v0.1.6 - github.com/sirupsen/logrus v1.4.2 github.com/urfave/cli v1.22.1 ) diff --git a/go.sum b/go.sum index b5f1dd9f9cc1..12ab50bbd053 100644 --- a/go.sum +++ b/go.sum @@ -1,55 +1,87 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/andygrunwald/go-gerrit v0.0.0-20190825170856-5959a9bf9ff8 h1:9PvNa6zH6gOW4VVfbAx5rjDLpxunG+RSaXQB+8TEv4w= github.com/andygrunwald/go-gerrit v0.0.0-20190825170856-5959a9bf9ff8/go.mod h1:0iuRQp6WJ44ts+iihy5E/WlPqfg5RNeQxOmzRkxCdtk= +github.com/apex/log v1.1.1 h1:BwhRZ0qbjYtTob0I+2M+smavV0kOC8XgcnGZcyL9liA= +github.com/apex/log v1.1.1/go.mod h1:Ls949n1HFtXfbDcjiTTFQqkVUrte0puoIBfO3SVgwOA= +github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= +github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= +github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs= github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= +github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= +github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= +github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= +github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= +github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index 14320276377f..1be379904b29 100644 --- a/main.go +++ b/main.go @@ -14,16 +14,13 @@ import ( "github.com/urfave/cli" - log "github.com/sirupsen/logrus" + "github.com/apex/log" + "github.com/apex/log/handlers/memory" + "github.com/apex/log/handlers/multi" + "github.com/apex/log/handlers/text" ) func main() { - // configure logging - log.SetFormatter(&log.TextFormatter{}) - //log.SetFormatter(&log.JSONFormatter{}) - log.SetOutput(os.Stdout) - log.SetLevel(log.DebugLevel) - var URL, username, password, projectName, branchName, submitQueueTag string var fetchOnly bool @@ -81,25 +78,33 @@ func main() { }, } + memoryLogHandler := memory.New() + l := &log.Logger{ + Handler: multi.New( + text.New(os.Stderr), + memoryLogHandler, + ), + Level: log.DebugLevel, + } + app.Action = func(c *cli.Context) error { - gerritClient, err := gerrit.NewClient(URL, username, password) + gerrit, err := gerrit.NewClient(l, URL, username, password, projectName, branchName) if err != nil { return err } - log.Printf("Successfully connected to gerrit at %s", URL) + log.Infof("Successfully connected to gerrit at %s", URL) - submitQueue := submitqueue.MakeSubmitQueue(gerritClient, projectName, branchName, submitQueueTag) - runner := submitqueue.NewRunner(submitQueue) + runner := submitqueue.NewRunner(l, gerrit, submitQueueTag) - handler := frontend.MakeFrontend(runner) + handler := frontend.MakeFrontend(memoryLogHandler, gerrit, runner) // fetch only on first run - runner.Trigger(true) + runner.Trigger(fetchOnly) // ticker go func() { for { - time.Sleep(time.Minute * 10) + time.Sleep(time.Minute * 5) runner.Trigger(fetchOnly) } }() @@ -111,7 +116,7 @@ func main() { server.ListenAndServe() if err != nil { - log.Fatal(err) + log.Fatalf(err.Error()) } return nil @@ -119,21 +124,9 @@ func main() { err := app.Run(os.Args) if err != nil { - log.Fatal(err) + log.Fatal(err.Error()) } - // mux := http.NewServeMux() - - // options := &gerrit.EventsLogOptions{} - // events, _, _, err := gerritClient.EventsLog.GetEvents(options) - // TODOS: - // - create submit queue user // - handle event log, either by accepting webhooks, or by streaming events? - - //n := negroni.Classic() - //n.UseHandler(mux) - - //fmt.Println("Listening on :3000…") - //http.ListenAndServe(":3000", n) } diff --git a/public/serie.tmpl.html b/public/serie.tmpl.html index 7db666c173ec..8c653946ce7e 100644 --- a/public/serie.tmpl.html +++ b/public/serie.tmpl.html @@ -1,6 +1,6 @@ {{ define "serie" }} <tr> -<td colspan="3" class="{{ if not (. | isAutoSubmittable) }}table-primary{{ else }}table-success{{ end }}">Serie with {{ len .ChangeSets }} changes</td> +<td colspan="3" class="table-success">Serie with {{ len .ChangeSets }} changes</td> </tr> {{ range $changeset := .ChangeSets }} {{ block "changeset" $changeset }}{{ end }} diff --git a/public/series.tmpl.html b/public/series.tmpl.html deleted file mode 100644 index 7e5ffd49c79a..000000000000 --- a/public/series.tmpl.html +++ /dev/null @@ -1,16 +0,0 @@ -{{ define "series" }} -<table class="table table-sm table-hover"> -<thead class="thead-light"> - <tr> - <th scope="col">Owner</th> - <th scope="col">Changeset</th> - <th scope="col">Flags</th> - </tr> -</thead> -<tbody> - {{ range $serie := . }} - {{ block "serie" $serie }}{{ end }} - {{ end }} -</tbody> -</table> -{{ end }} \ No newline at end of file diff --git a/public/submit-queue.tmpl.html b/public/submit-queue.tmpl.html index 27d8e9fdc69e..f64cdf24a170 100644 --- a/public/submit-queue.tmpl.html +++ b/public/submit-queue.tmpl.html @@ -42,63 +42,37 @@ </tr> <tr> <th scope="row">Currently running:</th> - <td> - {{ if .currentlyRunning }} - started at {{ .currentlyRunning.Format "2006-01-02 15:04:05 UTC" }} - {{ else }} - <span class="text-secondary">Not currently running</span> - {{ end }} - </td> + <td> + {{ if .currentlyRunning }}yes{{ else }}no{{ end }} + </td> + </tr> + <tr> + <th scope="row">HEAD:</th> + <td> + {{ if .currentlyRunning }}{{ .HEAD }}{{ else }}-{{ end }} + </td> </tr> </tbody> </table> - <h2 id="region-log">Log</h2> - <div id="history-accordion"> - {{ range $i, $result := .results }} - <div class="card"> - <div class="card-header"> - <h5> - <button class="btn btn-link" data-toggle="collapse" data-target="#history-collapse-{{ $i }}"> - Result Item {{ $i }}, {{ $result.StartTime.Format "2006-01-02 15:04:05 UTC"}} - {{ $result.EndTime.Format "2006-01-02 15:04:05 UTC"}} - </button> - </h5> - </div> - <div id="history-collapse-{{ $i }}" class="collapse {{ if eq $i 0 }} show{{ end }}" data-parent="#history-accordion"> - <div class="card-body"> - <table class="table"> - <tbody> - <tr> - <th scope="row">HEAD:</th> - <td><code>{{ .HEAD }}</code></td> - </tr> - {{ if $result.Error }} - <tr> - <th scope="row">Error:</th> - <td class="text-danger">{{ $result.Error }}</td> - </td> - </tr> - {{ end }} - <tr> - <th scope="row">Log:</th> - <td class="bg-dark text-white"> - {{ range $logEntry := $result.LogEntries }} - <code>{{ $logEntry }}</code><br /> - {{ end }} - </td> - </tr> - <tr> - <td colspan="2"> - {{ block "series" $result.Series}}{{ end }} - </td> - </tr> - </tbody> - </table> - </div> - </div> - </div> - {{ end }} - </div> - </div> <!-- .container --> + <h2 id="region-wipserie">wip Serie</h2> + {{ if .wipSerie }} + {{ block "serie" .wipSerie }}{{ end }} + {{ else }} + - + {{ end }} + + <h2 id="region-log">Log</h2> + <table class="table"> + <tbody> + <tr> + <td class="bg-dark text-white"> + {{ range $entry := .memory.Entries }} + <code>{{ $entry }}</code><br /> + {{ end }} + </td> + </tr> + </tbody> + </table> </body> </html> diff --git a/submitqueue/result.go b/submitqueue/result.go deleted file mode 100644 index a15c2968f30d..000000000000 --- a/submitqueue/result.go +++ /dev/null @@ -1,55 +0,0 @@ -package submitqueue - -import ( - "time" - - "github.com/sirupsen/logrus" -) - -// Problem: no inspection during the run -// Problem: record the state - -// Result contains all data necessary to inspect a previous run -// This includes the Series from that run, and all Log Entries collected. -// It also implements the interface required for logrus.Hook. -type Result struct { - LogEntries []*logrus.Entry - Series []Serie - Error error - startTime time.Time - HEAD string -} - -// MakeResult produces a new Result struct, -// and initializes startTime with the current time. -func MakeResult() *Result { - return &Result{ - startTime: time.Now(), - } -} - -// StartTime returns the startTime -func (r Result) StartTime() time.Time { - return r.startTime -} - -// EndTime returns the time of the latest log entry -func (r Result) EndTime() time.Time { - if len(r.LogEntries) == 0 { - return r.startTime - } - return r.LogEntries[len(r.LogEntries)-1].Time -} - -// Fire is called by logrus on each log event, -// we collect all log entries in the struct variable -func (r *Result) Fire(entry *logrus.Entry) error { - r.LogEntries = append(r.LogEntries, entry) - return nil -} - -// Levels is called by logrus to determine whether to Fire the handler. -// As we want to collect all log entries, we return logrus.AllLevels -func (r *Result) Levels() []logrus.Level { - return logrus.AllLevels -} diff --git a/submitqueue/runner.go b/submitqueue/runner.go index 4fd9134a0236..d85467b63d5d 100644 --- a/submitqueue/runner.go +++ b/submitqueue/runner.go @@ -1,60 +1,203 @@ package submitqueue import ( + "fmt" "sync" - "time" + + "github.com/apex/log" + + "github.com/tweag/gerrit-queue/gerrit" ) -// Runner supervises the submit queue and records historical data about it +// Runner is a struct existing across the lifetime of a single run of the submit queue +// it contains a mutex to avoid being run multiple times. +// In fact, it even cancels runs while another one is still in progress. +// It contains a Gerrit object facilitating access, a log object, the configured submit queue tag +// and a `wipSerie` (only populated if waiting for a rebase) type Runner struct { mut sync.Mutex - submitQueue *SubmitQueue - currentlyRunning *time.Time - results []*Result + currentlyRunning bool + wipSerie *gerrit.Serie + logger *log.Logger + gerrit *gerrit.Client + submitQueueTag string // the tag used to submit something to the submit queue } -// NewRunner initializes a new runner object -func NewRunner(sq *SubmitQueue) *Runner { +// NewRunner creates a new Runner struct +func NewRunner(logger *log.Logger, gerrit *gerrit.Client, submitQueueTag string) *Runner { return &Runner{ - submitQueue: sq, - results: []*Result{}, + logger: logger, + gerrit: gerrit, + submitQueueTag: submitQueueTag, } } -// GetState returns a copy of all the state for the frontend -func (r *Runner) GetState() (SubmitQueue, *time.Time, []*Result) { +// isAutoSubmittable determines if something could be autosubmitted, potentially requiring a rebase +// for this, it needs to: +// * have the auto-submit label +// * has +2 review +// * has +1 CI +func (r *Runner) isAutoSubmittable(s *gerrit.Serie) bool { + for _, c := range s.ChangeSets { + if c.Verified != 1 || c.CodeReviewed != 2 || !c.HasTag(r.submitQueueTag) { + return false + } + } + return true +} + +// IsCurrentlyRunning returns true if the runner is currently running +func (r *Runner) IsCurrentlyRunning() bool { + return r.currentlyRunning +} + +// GetWIPSerie returns the current wipSerie, if any, nil otherwiese +// Acquires a lock, so check with IsCurrentlyRunning first +func (r *Runner) GetWIPSerie() *gerrit.Serie { r.mut.Lock() - defer r.mut.Unlock() - return *r.submitQueue, r.currentlyRunning, r.results + defer func() { + r.mut.Unlock() + }() + return r.wipSerie } -// Trigger starts a new batch job -// TODO: make sure only one batch job is started at the same time -// if a batch job is already started, ignore the newest request -// TODO: be more granular in dry-run mode -func (r *Runner) Trigger(fetchOnly bool) { +// Trigger gets triggered periodically +func (r *Runner) Trigger(fetchOnly bool) error { + // TODO: If CI fails, remove the auto-submit labels => rules.pl + // Only one trigger can run at the same time r.mut.Lock() - if r.currentlyRunning != nil { - return + if r.currentlyRunning { + return fmt.Errorf("Already running, skipping") } - now := time.Now() - r.currentlyRunning = &now + r.currentlyRunning = true r.mut.Unlock() - defer func() { r.mut.Lock() - r.currentlyRunning = nil + r.currentlyRunning = false r.mut.Unlock() }() - result := r.submitQueue.Run(fetchOnly) + // isReady means a series is auto submittbale and rebased on HEAD + isReady := func(s *gerrit.Serie) bool { + return r.isAutoSubmittable(s) && r.gerrit.SerieIsRebasedOnHEAD(s) + } - r.mut.Lock() - // drop tail if size > 10 - if len(r.results) > 10 { - r.results = append([]*Result{result}, r.results[:9]...) - } else { - r.results = append([]*Result{result}, r.results...) + isAwaitingCI := func(s *gerrit.Serie) bool { + for _, c := range s.ChangeSets { + if !(c.Verified == 0 && c.CodeReviewed != 2 && c.HasTag(r.submitQueueTag)) { + return false + } + } + return true } - r.mut.Unlock() + + // Prepare the work by creating a local cache of gerrit state + r.gerrit.Refresh() + + // early return if we only want to fetch + if fetchOnly { + return nil + } + + if r.wipSerie != nil { + // refresh wipSerie with how it looks like in gerrit now + wipSerie := r.gerrit.FindSerie(func(s *gerrit.Serie) bool { + // the new wipSerie needs to have the same number of changesets + if len(r.wipSerie.ChangeSets) != len(s.ChangeSets) { + return false + } + // … and the same ChangeIDs. + for idx, c := range s.ChangeSets { + if r.wipSerie.ChangeSets[idx].ChangeID != c.ChangeID { + return false + } + } + return true + }) + if wipSerie == nil { + r.logger.WithField("wipSerie", r.wipSerie).Warn("wipSerie has disappeared") + r.wipSerie = nil + } else { + r.wipSerie = wipSerie + } + } + + for { + // initialize logger + r.logger.Info("Running") + if r.wipSerie != nil { + // if we have a wipSerie + l := r.logger.WithField("wipSerie", r.wipSerie) + l.Info("Checking wipSerie") + + if !r.gerrit.SerieIsRebasedOnHEAD(r.wipSerie) { + // check for chaos monkeys + l.Warnf("HEAD has moved to {} while still waiting for wipSerie, discarding it", r.gerrit.GetHEAD()) + r.wipSerie = nil + } else if isAwaitingCI(r.wipSerie) { + // the changeset is still awaiting for CI feedback + l.Info("keep waiting for wipSerie") + + // break the loop, take a look at it at the next trigger. + break + } else if isReady(r.wipSerie) { + // if the WIP changeset is ready (auto submittable and rebased on HEAD), submit + for _, changeset := range r.wipSerie.ChangeSets { + _, err := r.gerrit.SubmitChangeset(changeset) + if err != nil { + l.WithField("changeset", changeset).Error("error submitting changeset") + r.wipSerie = nil + return err + } + } + r.wipSerie = nil + } else { + // should never be reached?! + } + } + + r.logger.Info("Looking for series ready to submit") + // Find serie, that: + // * has the auto-submit label + // * has +2 review + // * has +1 CI + // * is rebased on master + serie := r.gerrit.FindSerie(isReady) + if serie != nil { + r.logger.WithField("serie", serie).Info("Found serie to submit without necessary rebase") + r.wipSerie = serie + continue + } + + // Find serie, that: + // * has the auto-submit label + // * has +2 review + // * has +1 CI + // * is NOT rebased on master + serie = r.gerrit.FindSerie(r.isAutoSubmittable) + if serie == nil { + r.logger.Info("nothing to do, going back to sleep.") + break + } + + l := r.logger.WithField("serie", serie) + l.Info("found serie, which needs a rebase") + // TODO: move into Client.RebaseSeries function + head := r.gerrit.GetHEAD() + for _, changeset := range serie.ChangeSets { + changeset, err := r.gerrit.RebaseChangeset(changeset, head) + if err != nil { + l.Error(err.Error()) + return err + } + head = changeset.CommitID + } + // it doesn't matter this serie isn't in its rebased state, + // we'll refetch it on the beginning of the next trigger anyways + r.wipSerie = serie + break + } + + r.logger.Info("Run complete") + return nil } diff --git a/submitqueue/submitqueue.go b/submitqueue/submitqueue.go deleted file mode 100644 index eb622db6832d..000000000000 --- a/submitqueue/submitqueue.go +++ /dev/null @@ -1,231 +0,0 @@ -package submitqueue - -import ( - "fmt" - - "github.com/tweag/gerrit-queue/gerrit" - - "github.com/sirupsen/logrus" -) - -// SubmitQueue contains a list of series, a gerrit connection, and some project configuration -type SubmitQueue struct { - Series []*Serie - gerrit gerrit.IClient - ProjectName string - BranchName string - HEAD string - SubmitQueueTag string // the tag used to submit something to the submit queue - URL string -} - -// MakeSubmitQueue builds a new submit queue -func MakeSubmitQueue(gerritClient gerrit.IClient, projectName string, branchName string, submitQueueTag string) *SubmitQueue { - return &SubmitQueue{ - Series: make([]*Serie, 0), - gerrit: gerritClient, - ProjectName: projectName, - BranchName: branchName, - SubmitQueueTag: submitQueueTag, - } -} - -// LoadSeries fills .Series by searching changesets, and assembling them to Series. -func (s *SubmitQueue) LoadSeries(log *logrus.Logger) error { - var queryString = fmt.Sprintf("status:open project:%s branch:%s", s.ProjectName, s.BranchName) - log.Debugf("Running query %s", queryString) - - // Download changesets from gerrit - changesets, err := s.gerrit.SearchChangesets(queryString) - if err != nil { - return err - } - - // Assemble to series - series, err := AssembleSeries(changesets, log) - if err != nil { - return err - } - - // Sort by size - s.Series = SortSeries(series) - return nil -} - -// TODO: clear submit queue tag if missing +1/+2? - -// IsAutoSubmittable returns true if a given Serie has all the necessary flags set -// meaning it would be fine to rebase and/or submit it. -// This means, every changeset needs to: -// - have the s.SubmitQueueTag hashtag -// - be verified (+1 by CI) -// - be code reviewed (+2 by a human) -func (s *SubmitQueue) IsAutoSubmittable(serie *Serie) bool { - return serie.FilterAllChangesets(func(c *gerrit.Changeset) bool { - return c.HasTag(s.SubmitQueueTag) && c.IsVerified && c.IsCodeReviewed - }) -} - -// GetChangesetURL returns the URL to view a given changeset -func (s *SubmitQueue) GetChangesetURL(changeset *gerrit.Changeset) string { - return fmt.Sprintf("%s/c/%s/+/%d", s.gerrit.GetBaseURL(), s.ProjectName, changeset.Number) -} - -// DoSubmit submits changes that can be submitted, -// and updates `Series` to contain the remaining ones -// Also updates `HEAD`. -func (s *SubmitQueue) DoSubmit(log *logrus.Logger) error { - var remainingSeries []*Serie - - // TODO: actually log more! - - for _, serie := range s.Series { - serieParentCommitIDs, err := serie.GetParentCommitIDs() - if err != nil { - return err - } - // we can only submit series with a single parent commit (otherwise they're not rebased) - if len(serieParentCommitIDs) != 1 { - return fmt.Errorf("%s has more than one parent commit, skipping", serie.String()) - } - - // if serie is auto-submittable and rebased on top of current master… - if s.IsAutoSubmittable(serie) && serieParentCommitIDs[0] == s.HEAD { - // submit the last changeset of the series, which submits intermediate ones too - _, err := s.gerrit.SubmitChangeset(serie.ChangeSets[len(serie.ChangeSets)-1]) - if err != nil { - // this might fail, for various reasons: - // - developers could have updated the changeset meanwhile, clearing +1/+2 bits - // - master might have advanced, so this changeset isn't rebased on top of master - // TODO: we currently bail out entirely, but should be fine on the - // next loop. We might later want to improve the logic to be a bit more - // smarter (like log and try with the next one) - return err - } - // advance head to the leaf of the current serie for the next iteration - newHead, err := serie.GetLeafCommitID() - if err != nil { - return err - } - s.HEAD = newHead - } else { - remainingSeries = append(remainingSeries, serie) - } - } - - s.Series = remainingSeries - return nil -} - -// DoRebase rebases the next auto-submittable series on top of current HEAD -// they are still ordered by series size -// After a DoRebase, consumers are supposed to fetch state again via LoadSeries, -// as things most likely have changed, and error handling during partially failed rebases -// is really tricky -func (s *SubmitQueue) DoRebase(log *logrus.Logger) error { - if s.HEAD == "" { - return fmt.Errorf("current HEAD is an empty string, bailing out") - } - for _, serie := range s.Series { - logger := log.WithFields(logrus.Fields{ - "serie": serie, - }) - if !s.IsAutoSubmittable(serie) { - logger.Debug("skipping non-auto-submittable series") - continue - } - - logger.Infof("rebasing on top of %s", s.HEAD) - _, err := s.RebaseSerie(serie, s.HEAD) - if err != nil { - // We skip trivial rebase errors instead of bailing out. - // TODO: we might want to remove s.SubmitQueueTag from the changeset, - // but even without doing it, - // we're merly spanning, and won't get stuck in trying to rebase the same - // changeset over and over again, as some other changeset will likely succeed - // with rebasing and will be merged by DoSubmit. - logger.Warnf("failure while rebasing, continuing with next one: %s", err) - continue - } else { - logger.Info("success rebasing on top of %s", s.HEAD) - break - } - } - - return nil -} - -// Run starts the submit and rebase logic. -func (s *SubmitQueue) Run(fetchOnly bool) *Result { - r := MakeResult() - //TODO: log decisions made and add to some ring buffer - var err error - - log := logrus.New() - log.AddHook(r) - - commitID, err := s.gerrit.GetHEAD(s.ProjectName, s.BranchName) - if err != nil { - log.Errorf("Unable to retrieve HEAD of branch %s at project %s: %s", s.BranchName, s.ProjectName, err) - r.Error = err - return r - } - s.HEAD = commitID - r.HEAD = commitID - - err = s.LoadSeries(log) - if err != nil { - r.Error = err - return r - } - - // copy series to result object - for _, serie := range s.Series { - r.Series = append(r.Series, *serie) - } - - if len(s.Series) == 0 { - // Nothing to do! - log.Warn("Nothing to do here") - return r - } - if fetchOnly { - return r - } - err = s.DoSubmit(log) - if err != nil { - r.Error = err - return r - } - err = s.DoRebase(log) - if err != nil { - r.Error = err - return r - } - return r -} - -// RebaseSerie rebases a whole serie on top of a given ref -// TODO: only rebase a single changeset. we don't really want to join disconnected series, by rebasing them on top of each other. -func (s *SubmitQueue) RebaseSerie(serie *Serie, ref string) (*Serie, error) { - newSeries := &Serie{ - ChangeSets: make([]*gerrit.Changeset, len(serie.ChangeSets)), - } - - rebaseOnto := ref - for _, changeset := range serie.ChangeSets { - newChangeset, err := s.gerrit.RebaseChangeset(changeset, rebaseOnto) - - if err != nil { - // uh-oh… - // TODO: think about error handling - // TODO: remove the submit queue tag if the rebase fails (but only then, not on other errors) - return newSeries, err - } - newSeries.ChangeSets = append(newSeries.ChangeSets, newChangeset) - - // the next changeset should be rebased on top of the current commit - rebaseOnto = newChangeset.CommitID - } - return newSeries, nil -} |