Use Runner

This revamps code quite a bit. Series handling has been moved into the
gerrit client, it also handles caching.

The Runner logic itself has been greatly simplified.

The runner logic has been moved into the runner.go, submitqueue.go is
gone.

The "per-run result object" concept has been dropped - we instead just
use annotated logs.

Also, we switched to apex/log
This commit is contained in:
Florian Klink 2019-12-02 10:00:32 +01:00
parent 7bafef7a84
commit 04a24a0c60
14 changed files with 486 additions and 537 deletions

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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 {

2
go.mod
View file

@ -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
)

58
go.sum
View file

@ -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=

49
main.go
View file

@ -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)
}

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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>

View file

@ -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
}

View file

@ -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) {
r.mut.Lock()
defer r.mut.Unlock()
return *r.submitQueue, r.currentlyRunning, r.results
// 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
}
// 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) {
// 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()
if r.currentlyRunning != nil {
return
defer func() {
r.mut.Unlock()
}()
return r.wipSerie
}
// 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 {
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)
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...)
// 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.Unlock()
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
}
// 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
}

View file

@ -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
}