diff options
-rw-r--r-- | frontend/frontend.go | 25 | ||||
-rw-r--r-- | gerrit/client.go | 14 | ||||
-rw-r--r-- | main.go | 42 | ||||
-rw-r--r-- | public/submit-queue.tmpl.html | 11 | ||||
-rw-r--r-- | submitqueue/runner.go | 60 | ||||
-rw-r--r-- | submitqueue/series.go | 8 | ||||
-rw-r--r-- | submitqueue/submitqueue.go | 82 |
7 files changed, 186 insertions, 56 deletions
diff --git a/frontend/frontend.go b/frontend/frontend.go index 240c04ae3e2d..0718bb136d1f 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/rakyll/statik/fs" + "github.com/tweag/gerrit-queue/gerrit" _ "github.com/tweag/gerrit-queue/statik" // register static assets "github.com/tweag/gerrit-queue/submitqueue" ) @@ -21,13 +22,13 @@ type Frontend struct { } //loadTemplate loads a single template from statikFS and returns a template object -func loadTemplate(templateName string) (*template.Template, error) { +func loadTemplate(templateName string, funcMap template.FuncMap) (*template.Template, error) { statikFS, err := fs.New() if err != nil { return nil, err } - tmpl := template.New(templateName) + tmpl := template.New(templateName).Funcs(funcMap) r, err := statikFS.Open("/" + templateName) if err != nil { return nil, err @@ -41,9 +42,20 @@ func loadTemplate(templateName string) (*template.Template, error) { } // MakeFrontend configures the router and returns a new Frontend struct -func MakeFrontend(router *gin.Engine, submitQueue *submitqueue.SubmitQueue) *Frontend { +func MakeFrontend(runner *submitqueue.Runner, submitQueue *submitqueue.SubmitQueue) *Frontend { + router := gin.Default() + + funcMap := template.FuncMap{ + "isAutoSubmittable": func(serie *submitqueue.Serie) bool { + return submitQueue.IsAutoSubmittable(serie) + }, + "changesetURL": func(changeset *gerrit.Changeset) string { + return submitQueue.GetChangesetURL(changeset) + }, + } + + tmpl := template.Must(loadTemplate("submit-queue.tmpl.html", funcMap)) - tmpl := template.Must(loadTemplate("submit-queue.tmpl.html")) router.SetHTMLTemplate(tmpl) router.GET("/submit-queue.json", func(c *gin.Context) { @@ -77,7 +89,6 @@ func MakeFrontend(router *gin.Engine, submitQueue *submitqueue.SubmitQueue) *Fro } } -// Run starts the webserver on a given address -func (f *Frontend) Run(addr string) error { - return f.Router.Run(addr) +func (f *Frontend) ServeHTTP(w http.ResponseWriter, r *http.Request) { + f.Router.ServeHTTP(w, r) } diff --git a/gerrit/client.go b/gerrit/client.go index 5a13befe5463..c65b5016c1ff 100644 --- a/gerrit/client.go +++ b/gerrit/client.go @@ -17,13 +17,15 @@ type IClient interface { SubmitChangeset(changeset *Changeset) (*Changeset, error) RebaseChangeset(changeset *Changeset, ref string) (*Changeset, error) RemoveTag(changeset *Changeset, tag string) (*Changeset, error) + GetBaseURL() string } var _ IClient = &Client{} // Client provides some ways to interact with a gerrit instance type Client struct { - client *goGerrit.Client + client *goGerrit.Client + baseURL string } // NewClient initializes a new gerrit client @@ -38,7 +40,10 @@ func NewClient(URL, username, password string) (*Client, error) { if err != nil { return nil, err } - return &Client{client: goGerritClient}, nil + return &Client{ + client: goGerritClient, + baseURL: URL, + }, nil } // SearchChangesets fetches a list of changesets matching a passed query string @@ -117,3 +122,8 @@ func (gerrit *Client) RemoveTag(changeset *Changeset, tag string) (*Changeset, e // 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 +} diff --git a/main.go b/main.go index 9bd8cd1feab1..1f64c7759132 100644 --- a/main.go +++ b/main.go @@ -4,16 +4,16 @@ package main import ( "os" + "time" + + "net/http" "github.com/tweag/gerrit-queue/frontend" "github.com/tweag/gerrit-queue/gerrit" "github.com/tweag/gerrit-queue/submitqueue" - "github.com/gin-gonic/gin" "github.com/urfave/cli" - "fmt" - log "github.com/sirupsen/logrus" ) @@ -88,31 +88,29 @@ func main() { log.Printf("Successfully connected to gerrit at %s", URL) submitQueue := submitqueue.MakeSubmitQueue(gerritClient, projectName, branchName, submitQueueTag) + runner := submitqueue.NewRunner(submitQueue) - router := gin.Default() - frontend := frontend.MakeFrontend(router, &submitQueue) + handler := frontend.MakeFrontend(runner, submitQueue) - err = submitQueue.LoadSeries() - if err != nil { - log.Errorf("Error loading submit queue: %s", err) - } + // fetch only on first run + runner.Trigger(true) - fmt.Println() - fmt.Println() - fmt.Println() - fmt.Println() - for _, serie := range submitQueue.Series { - fmt.Println(fmt.Sprintf("%s", serie)) - for _, changeset := range serie.ChangeSets { - fmt.Println(fmt.Sprintf(" - %s", changeset.String())) + // ticker + go func() { + for { + time.Sleep(time.Minute * 10) + runner.Trigger(fetchOnly) } - fmt.Println() - } + }() - frontend.Run(":8080") + server := http.Server{ + Addr: ":8080", + Handler: handler, + } - if fetchOnly { - //return backlog.Run() + server.ListenAndServe() + if err != nil { + log.Fatal(err) } return nil diff --git a/public/submit-queue.tmpl.html b/public/submit-queue.tmpl.html index f1320a0c2dde..cad001cd3dbd 100644 --- a/public/submit-queue.tmpl.html +++ b/public/submit-queue.tmpl.html @@ -9,13 +9,17 @@ <h1>Gerrit Submit Queue</h1> <h2>{{ .projectName }}/{{ .branchName }} is at {{ printf "%.7s" .HEAD }}</h2> <h2>Current Queue:</h2> + <div class="card-columns"> + {{ range $serie := .series }} + + <div class="card" style="width: 18rem"> <div class="list-group"> {{ range $changeset := $serie.ChangeSets}} - <div class="list-group-item"> + <div class="list-group-item{{ if not ($serie | isAutoSubmittable) }} disabled{{ end }}"> <div class="d-flex w-100 justify-content-between"> <h5>{{ $changeset.Subject }}</h5> - <small>#{{ $changeset.Number }}</small> + <small><a href="{{ changesetURL $changeset }}" target="_blank">#{{ $changeset.Number }}</a></small> </div> <div class="d-flex w-100 justify-content-between"> <small>{{ $changeset.OwnerName }}</small> @@ -32,7 +36,8 @@ </div> {{ end }} </div> - <br /> + </div> {{ end }} + </div> </body> </html> diff --git a/submitqueue/runner.go b/submitqueue/runner.go new file mode 100644 index 000000000000..2c84a7d69b7f --- /dev/null +++ b/submitqueue/runner.go @@ -0,0 +1,60 @@ +package submitqueue + +import ( + "sync" + "time" +) + +// Runner supervises the submit queue and records historical data about it +type Runner struct { + mut sync.Mutex + submitQueue *SubmitQueue + currentlyRunning *time.Time + results []*Result +} + +func NewRunner(sq *SubmitQueue) *Runner { + return &Runner{ + submitQueue: sq, + results: []*Result{}, + } +} + +// For the frontend to consume the data +// TODO: extend to return all the submitQueue results +func (r *Runner) GetResults() (*time.Time, []*Result) { + r.mut.Lock() + defer r.mut.Unlock() + return r.currentlyRunning, r.results +} + +// 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) { + r.mut.Lock() + if r.currentlyRunning != nil { + return + } + now := time.Now() + r.currentlyRunning = &now + r.mut.Unlock() + + defer func() { + r.mut.Lock() + r.currentlyRunning = nil + 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...) + } + r.mut.Unlock() +} diff --git a/submitqueue/series.go b/submitqueue/series.go index 42abff2d49be..c607e3ec2135 100644 --- a/submitqueue/series.go +++ b/submitqueue/series.go @@ -5,7 +5,7 @@ import ( "github.com/tweag/gerrit-queue/gerrit" - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" ) // AssembleSeries consumes a list of `Changeset`, and groups them together to series @@ -17,12 +17,12 @@ import ( // 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) ([]*Serie, error) { +func AssembleSeries(changesets []*gerrit.Changeset, log *logrus.Logger) ([]*Serie, error) { series := make([]*Serie, 0) mapLeafToSerie := make(map[string]*Serie, 0) for _, changeset := range changesets { - logger := log.WithFields(log.Fields{ + logger := log.WithFields(logrus.Fields{ "changeset": changeset.String(), }) @@ -90,7 +90,7 @@ func AssembleSeries(changesets []*gerrit.Changeset) ([]*Serie, error) { // Check integrity, just to be on the safe side. for _, serie := range series { - logger := log.WithFields(log.Fields{ + logger := log.WithFields(logrus.Fields{ "serie": serie.String(), }) logger.Debugf("checking integrity") diff --git a/submitqueue/submitqueue.go b/submitqueue/submitqueue.go index 922ca323483d..0b61143439f9 100644 --- a/submitqueue/submitqueue.go +++ b/submitqueue/submitqueue.go @@ -2,10 +2,11 @@ package submitqueue import ( "fmt" + "time" "github.com/tweag/gerrit-queue/gerrit" - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" ) // SubmitQueue contains a list of series, a gerrit connection, and some project configuration @@ -16,11 +17,12 @@ type SubmitQueue struct { 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{ +func MakeSubmitQueue(gerritClient gerrit.IClient, projectName string, branchName string, submitQueueTag string) *SubmitQueue { + return &SubmitQueue{ Series: make([]*Serie, 0), gerrit: gerritClient, ProjectName: projectName, @@ -30,7 +32,7 @@ func MakeSubmitQueue(gerritClient gerrit.IClient, projectName string, branchName } // LoadSeries fills .Series by searching changesets, and assembling them to Series. -func (s *SubmitQueue) LoadSeries() error { +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) @@ -41,7 +43,7 @@ func (s *SubmitQueue) LoadSeries() error { } // Assemble to series - series, err := AssembleSeries(changesets) + series, err := AssembleSeries(changesets, log) if err != nil { return err } @@ -75,12 +77,19 @@ func (s *SubmitQueue) IsAutoSubmittable(serie *Serie) bool { }) } +// 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() error { +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 { @@ -124,12 +133,12 @@ func (s *SubmitQueue) DoSubmit() error { // 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() error { +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(log.Fields{ + logger := log.WithFields(logrus.Fields{ "serie": serie, }) if !s.IsAutoSubmittable(serie) { @@ -157,36 +166,73 @@ func (s *SubmitQueue) DoRebase() error { return nil } +// Problem: no inspection during the run +// Problem: record the state + +type Result struct { + LogEntries []*logrus.Entry + Series []Serie + Error error +} + +func (r Result) StartTime() time.Time { + return r.LogEntries[0].Time +} + +func (r Result) EndTime() time.Time { + return r.LogEntries[len(r.LogEntries)-1].Time +} + +func (r *Result) Fire(entry *logrus.Entry) error { + r.LogEntries = append(r.LogEntries, entry) + return nil +} + +func (r *Result) Levels() []logrus.Level { + return logrus.AllLevels +} + // Run starts the submit and rebase logic. -func (s *SubmitQueue) Run() error { +func (s *SubmitQueue) Run(fetchOnly bool) *Result { + r := &Result{} //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) - return err + r.Error = err + return r } s.HEAD = commitID - err = s.LoadSeries() + err = s.LoadSeries(log) if err != nil { - return err + r.Error = err + return r } if len(s.Series) == 0 { // Nothing to do! log.Warn("Nothing to do here") - return nil + return r + } + if fetchOnly { + return r } - err = s.DoSubmit() + err = s.DoSubmit(log) if err != nil { - return err + r.Error = err + return r } - err = s.DoRebase() + err = s.DoRebase(log) if err != nil { - return err + r.Error = err + return r } - return nil + return r } // RebaseSerie rebases a whole serie on top of a given ref |