chore(third_party/gerrit-queue): move to tvl overlay

Bump to a version including https://github.com/flokli/gerrit-queue/pull/15

Change-Id: Ie316498ca2c608e5489901c5705ce5f2dc047f29
Reviewed-on: https://cl.tvl.fyi/c/depot/+/9808
Reviewed-by: tazjin <tazjin@tvl.su>
Tested-by: BuildkiteCI
This commit is contained in:
Florian Klink 2023-10-20 13:18:02 +01:00 committed by flokli
parent 2513ddd2b7
commit 9a1e5cf4c7
21 changed files with 20 additions and 1585 deletions

View file

@ -35,7 +35,7 @@ in
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${depot.third_party.gerrit-queue}/bin/gerrit-queue";
ExecStart = "${pkgs.gerrit-queue}/bin/gerrit-queue";
DynamicUser = true;
Restart = "always";
EnvironmentFile = cfg.secretsFile;

View file

@ -1,4 +0,0 @@
#!/usr/bin/env bash
export GOPATH=~/go
go generate
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -a -ldflags '-extldflags \"-static\"' -o gerrit-queue

View file

@ -1,13 +0,0 @@
steps:
- command: |
. /var/lib/buildkite-agent/.nix-profile/etc/profile.d/nix.sh
# produces a ./gerrit-queue
nix-shell --run ./.buildkite/build.sh
mkdir -p out
mv ./gerrit-queue out/gerrit-queue-$(git describe --tags)
label: "Build (linux/amd64)"
timeout: 30
artifact_paths:
- "out/*"

View file

@ -1,4 +0,0 @@
/.vscode
/statik
/.envrc.private
/gerrit-queue

View file

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1,80 +0,0 @@
# gerrit-queue
This daemon automatically rebases and submits changesets from a Gerrit
instance, ensuring they still pass CI.
In a usual gerrit setup with a linear master history, different developers
await CI feedback on a rebased changeset, then one clicks submit, and
effectively makes everybody else rebase again. `gerrit-queue` is meant to
remove these races to master.
Developers can set the `Autosubmit` label to `+1` on all changesets in a series,
and if all preconditions on are met ("submittable" in gerrit speech, this
usually means passing CI and passing Code Review), `gerrit-queue` takes care of
rebasing and submitting it to master
## How it works
Gerrit only knows about Changesets (and some relations to other changesets),
but usually developers think in terms of multiple changesets.
### Fetching changesets
`gerrit-queue` fetches all changesets from gerrit, and tries to identify these
chains of changesets. We call them `Series`. All changesets need to have strict
parent/child relationships to be detected (so if only half of the stack gets
rebased by the Gerrit Web interface, these are considered individual series.
Series are sorted by the number of changesets in them. This ensures longer
series are merged faster, and less rebases are triggered. In the future, this
might be extended to other metrics.
### Submitting changesets
The submitqueue has a Trigger() function, which gets periodically executed.
It can keep a reference to one single serie across multiple runs. This is
necessary if it previously rebased one serie to current HEAD and needs to wait
some time until CI feedback is there. If it wouldn't keep that state, it would
pick another series (with +1 from CI) and trigger a rebase on that one, so
depending on CI run times and trigger intervals, if not keepig this information
it'd end up rebasing all unrebased changesets on the same HEAD, and then just
pick one, instead of waiting for the one to finish.
The Trigger() function first instructs the gerrit client to fetch changesets
and assemble series.
If there is a `wipSerie` from a previous run, we check if it can still be found
in the newly assembled list of series (it still needs to contain the same
number of series. Commit IDs may differ, because the code doesn't reassemble a
`wipSerie` after scheduling a rebase.
If the `wipSerie` could be refreshed, we update the pointer with the newly
assembled series. If we couldn't find it, we drop it.
Now, we enter the main for loop. The first half of the loop checks various
conditions of the current `wipSerie`, and if successful, does the submit
("Submit phase"), the second half will pick a suitable new `wipSerie`, and
potentially do a rebase ("Pick phase").
#### Submit phase
We check if there is an existing `wipSerie`. If there isn't, we immediately go to
the "pick" phase.
The `wipSerie` still needs to be rebased on `HEAD` (otherwise, the submit queue
advanced outside of gerrit), and should not fail CI (logical merge conflict) -
otherwise we discard it, and continue with the picking phase.
If the `wipSerie` still contains a changeset awaiting CI feedback, we `return`
from the `Trigger()` function (and go back to sleep).
If the changeset is "submittable" in gerrit speech, and has the necessary
submit queue tag set, we submit it.
#### Pick phase
The pick phase finds a new `wipSerie`. It'll first try to find one that already
is rebased on the current `HEAD` (so the loop can just continue, and the next
submit phase simply submit), and otherwise fall back to a not-yet-rebased
serie. Because the rebase mandates waiting for CI, the code `return`s the
`Trigger()` function, so it'll be called again after waiting some time.
## Compile and Run
```sh
go generate
GERRIT_PASSWORD=mypassword go run main.go --url https://gerrit.mydomain.com --username myuser --project myproject
```

View file

@ -1,14 +0,0 @@
{ pkgs, lib, ... }:
pkgs.buildGoModule {
pname = "gerrit-queue";
version = "master";
vendorHash = "sha256:0n5h7j416yb2mwic9c3rhqza64jlvl7iw507r9mkw3jadn4whm7a";
src = ./.;
meta = with lib; {
description = "Gerrit submit bot";
homepage = "https://github.com/tweag/gerrit-queue";
license = licenses.asl20;
};
}

View file

@ -1,113 +0,0 @@
package frontend
import (
"embed"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"html/template"
"github.com/apex/log"
"github.com/tweag/gerrit-queue/gerrit"
"github.com/tweag/gerrit-queue/misc"
"github.com/tweag/gerrit-queue/submitqueue"
)
//go:embed templates
var templates embed.FS
//loadTemplate loads a list of templates, relative to the templates root, and a
//FuncMap, and returns a template object
func loadTemplate(templateNames []string, funcMap template.FuncMap) (*template.Template, error) {
if len(templateNames) == 0 {
return nil, fmt.Errorf("templateNames can't be empty")
}
tmpl := template.New(templateNames[0]).Funcs(funcMap)
for _, templateName := range templateNames {
r, err := templates.Open("/" + templateName)
if err != nil {
return nil, err
}
defer r.Close()
contents, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
tmpl, err = tmpl.Parse(string(contents))
if err != nil {
return nil, err
}
}
return tmpl, nil
}
// MakeFrontend returns a http.Handler
func MakeFrontend(rotatingLogHandler *misc.RotatingLogHandler, gerritClient *gerrit.Client, runner *submitqueue.Runner) http.Handler {
projectName := gerritClient.GetProjectName()
branchName := gerritClient.GetBranchName()
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
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{
"changesetURL": func(changeset *gerrit.Changeset) string {
return gerritClient.GetChangesetURL(changeset)
},
"levelToClasses": func(level log.Level) string {
switch level {
case log.DebugLevel:
return "text-muted"
case log.InfoLevel:
return "text-info"
case log.WarnLevel:
return "text-warning"
case log.ErrorLevel:
return "text-danger"
case log.FatalLevel:
return "text-danger"
default:
return "text-white"
}
},
"fieldsToJSON": func(fields log.Fields) string {
jsonData, _ := json.Marshal(fields)
return string(jsonData)
},
}
tmpl := template.Must(loadTemplate([]string{
"index.tmpl.html",
"serie.tmpl.html",
"changeset.tmpl.html",
}, funcMap))
tmpl.ExecuteTemplate(w, "index.tmpl.html", map[string]interface{}{
// Config
"projectName": projectName,
"branchName": branchName,
// State
"currentlyRunning": currentlyRunning,
"wipSerie": wipSerie,
"HEAD": HEAD,
// History
"memory": rotatingLogHandler,
})
})
return mux
}

View file

@ -1,15 +0,0 @@
{{ define "changeset" }}
<tr>
<td>{{ .OwnerName }}</td>
<td>
<strong>{{ .Subject }}</strong> (<a href="{{ changesetURL . }}" target="_blank">#{{ .Number }}</a>)<br />
<small><code>{{ .CommitID }}</code></small>
</td>
<td>
<span>
{{ if .IsVerified }}<span class="badge badge-success badge-pill">+1 (CI)</span>{{ end }}
{{ if .IsCodeReviewed }}<span class="badge badge-info badge-pill">+2 (CR)</span>{{ end }}
</span>
</td>
</tr>
{{ end }}

View file

@ -1,76 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Gerrit Submit Queue</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha256-CjSoeELFOcH0/uxWu6mC/Vlrc1AARqbm/jiiImDGV3s=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha256-YLGeXaapI0/5IgZopewRJcFXomhRMlYYjugPLSyNjTY=" crossorigin="anonymous" />
</head>
<body>
<nav class="navbar sticky-top navbar-expand-sm navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">Gerrit Submit Queue</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="#region-info">Info</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#region-wipserie">WIP Serie</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#region-log">Log</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
<h2 id="region-info">Info</h2>
<table class="table">
<tbody>
<tr>
<th scope="row">Project Name:</th>
<td>{{ .projectName }}</td>
</tr>
<tr>
<th scope="row">Branch Name:</th>
<td>{{ .branchName }}</td>
</tr>
<tr>
<th scope="row">Currently running:</th>
<td>
{{ if .currentlyRunning }}yes{{ else }}no{{ end }}
</td>
</tr>
<tr>
<th scope="row">HEAD:</th>
<td>
{{ if .HEAD }}{{ .HEAD }}{{ else }}-{{ end }}
</td>
</tr>
</tbody>
</table>
<h2 id="region-wipserie">WIP Serie</h2>
{{ if .wipSerie }}
{{ block "serie" .wipSerie }}{{ end }}
{{ else }}
-
{{ end }}
<h2 id="region-log">Log</h2>
{{ range $entry := .memory.Entries }}
<div class="d-flex flex-row bg-dark {{ levelToClasses $entry.Level }} text-monospace">
<div class="p-2"><small>{{ $entry.Timestamp.Format "2006-01-02 15:04:05 UTC"}}</small></div>
<div class="p-2 flex-grow-1"><small><strong>{{ $entry.Message }}</strong></small></div>
</div>
<div class="bg-dark {{ levelToClasses $entry.Level }} text-monospace text-break" style="padding-left: 4rem">
<small>{{ fieldsToJSON $entry.Fields }}</small>
</div>
{{ end }}
</body>
</html>

View file

@ -1,19 +0,0 @@
{{ define "serie" }}
<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>
<tr>
<td colspan="3" class="table-success">Serie with {{ len .ChangeSets }} changes</td>
</tr>
{{ range $changeset := .ChangeSets }}
{{ block "changeset" $changeset }}{{ end }}
{{ end }}
</tbody>
</table>
{{ end }}

View file

@ -1,117 +0,0 @@
package gerrit
import (
"bytes"
"fmt"
goGerrit "github.com/andygrunwald/go-gerrit"
"github.com/apex/log"
)
// Changeset represents a single changeset
// Relationships between different changesets are described in Series
type Changeset struct {
changeInfo *goGerrit.ChangeInfo
ChangeID string
Number int
Verified int
CodeReviewed int
Autosubmit int
Submittable bool
CommitID string
ParentCommitIDs []string
OwnerName string
Subject string
}
// MakeChangeset creates a new Changeset object out of a goGerrit.ChangeInfo object
func MakeChangeset(changeInfo *goGerrit.ChangeInfo) *Changeset {
return &Changeset{
changeInfo: changeInfo,
ChangeID: changeInfo.ChangeID,
Number: changeInfo.Number,
Verified: labelInfoToInt(changeInfo.Labels["Verified"]),
CodeReviewed: labelInfoToInt(changeInfo.Labels["Code-Review"]),
Autosubmit: labelInfoToInt(changeInfo.Labels["Autosubmit"]),
Submittable: changeInfo.Submittable,
CommitID: changeInfo.CurrentRevision, // yes, this IS the commit ID.
ParentCommitIDs: getParentCommitIDs(changeInfo),
OwnerName: changeInfo.Owner.Name,
Subject: changeInfo.Subject,
}
}
// IsAutosubmit returns true if the changeset is intended to be
// automatically submitted by gerrit-queue.
//
// This is determined by the Change Owner setting +1 on the
// "Autosubmit" label.
func (c *Changeset) IsAutosubmit() bool {
return c.Autosubmit == 1
}
// 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")
b.WriteString(fmt.Sprintf("(commitID: %.7s, author: %s, subject: %s, submittable: %v)",
c.CommitID, c.OwnerName, c.Subject, c.Submittable))
return b.String()
}
// FilterChangesets filters a list of Changeset by a given filter function
func FilterChangesets(changesets []*Changeset, f func(*Changeset) bool) []*Changeset {
newChangesets := make([]*Changeset, 0)
for _, changeset := range changesets {
if f(changeset) {
newChangesets = append(newChangesets, changeset)
} else {
log.WithField("changeset", changeset.String()).Debug("dropped by filter")
}
}
return newChangesets
}
// 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
// There is usually only one parent commit ID, except for merge commits.
func getParentCommitIDs(changeInfo *goGerrit.ChangeInfo) []string {
// obtain the RevisionInfo object
revisionInfo := changeInfo.Revisions[changeInfo.CurrentRevision]
// obtain the Commit object
commit := revisionInfo.Commit
commitIDs := make([]string, len(commit.Parents))
for i, commit := range commit.Parents {
commitIDs[i] = commit.Commit
}
return commitIDs
}

View file

@ -1,220 +0,0 @@
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",
"SUBMITTABLE",
}
// IClient defines the gerrit.Client interface
type IClient interface {
Refresh() error
GetHEAD() string
GetBaseURL() string
GetChangesetURL(changeset *Changeset) string
SubmitChangeset(changeset *Changeset) (*Changeset, error)
RebaseChangeset(changeset *Changeset, ref string) (*Changeset, error)
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
logger *log.Logger
baseURL string
projectName string
branchName string
series []*Serie
head string
}
// NewClient initializes a new gerrit client
func NewClient(logger *log.Logger, URL, username, password, projectName, branchName string) (*Client, error) {
urlParsed, err := url.Parse(URL)
if err != nil {
return nil, err
}
urlParsed.User = url.UserPassword(username, password)
goGerritClient, err := goGerrit.NewClient(urlParsed.String(), nil)
if err != nil {
return nil, err
}
return &Client{
client: goGerritClient,
baseURL: URL,
logger: logger,
projectName: projectName,
branchName: branchName,
}, nil
}
// 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.Infof("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
changes, _, err := c.client.Changes.QueryChanges(opt)
if err != nil {
return nil, err
}
changesets = make([]*Changeset, 0)
for _, change := range *changes {
changesets = append(changesets, MakeChangeset(&change))
}
return changesets, nil
}
// 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 (c *Client) fetchChangeset(changeID string) (*Changeset, error) {
opt := goGerrit.ChangeOptions{}
opt.AdditionalFields = []string{"LABELS", "DETAILED_ACCOUNTS"}
changeInfo, _, err := c.client.Changes.GetChange(changeID, &opt)
if err != nil {
return nil, err
}
return MakeChangeset(changeInfo), nil
}
// SubmitChangeset submits a given changeset, and returns a changeset afterwards.
func (c *Client) SubmitChangeset(changeset *Changeset) (*Changeset, error) {
changeInfo, _, err := c.client.Changes.SubmitChange(changeset.ChangeID, &goGerrit.SubmitInput{})
if err != nil {
return nil, err
}
c.head = changeInfo.CurrentRevision
return c.fetchChangeset(changeInfo.ChangeID)
}
// RebaseChangeset rebases a given changeset on top of a given ref
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 c.fetchChangeset(changeInfo.ChangeID)
}
// GetBaseURL returns the gerrit base URL
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,112 +0,0 @@
package gerrit
import (
"fmt"
"strings"
"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 []*Changeset
}
// GetParentCommitIDs returns the parent commit IDs
func (s *Serie) GetParentCommitIDs() ([]string, error) {
if len(s.ChangeSets) == 0 {
return nil, fmt.Errorf("Can't return parent on a serie with zero ChangeSets")
}
return s.ChangeSets[0].ParentCommitIDs, nil
}
// GetLeafCommitID returns the commit id of the last commit in ChangeSets
func (s *Serie) GetLeafCommitID() (string, error) {
if len(s.ChangeSets) == 0 {
return "", fmt.Errorf("Can't return leaf on a serie with zero ChangeSets")
}
return s.ChangeSets[len(s.ChangeSets)-1].CommitID, nil
}
// CheckIntegrity checks that the series contains a properly ordered and connected chain of commits
func (s *Serie) CheckIntegrity() error {
logger := log.WithField("serie", s)
// an empty serie is invalid
if len(s.ChangeSets) == 0 {
return fmt.Errorf("An empty serie is invalid")
}
previousCommitID := ""
for i, changeset := range s.ChangeSets {
// we can't really check the parent of the first commit
// so skip verifying that one
logger.WithFields(log.Fields{
"changeset": changeset.String(),
"previousCommitID": fmt.Sprintf("%.7s", previousCommitID),
}).Debug(" - verifying changeset")
parentCommitIDs := changeset.ParentCommitIDs
if len(parentCommitIDs) == 0 {
return fmt.Errorf("Changesets without any parent are not supported")
}
// we don't check parents of the first changeset in a series
if i != 0 {
if len(parentCommitIDs) != 1 {
return fmt.Errorf("Merge commits in the middle of a series are not supported (only at the beginning)")
}
if parentCommitIDs[0] != previousCommitID {
return fmt.Errorf("changesets parent commit id doesn't match previous commit id")
}
}
// update previous commit id for the next loop iteration
previousCommitID = changeset.CommitID
}
return nil
}
// 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 *Changeset) bool) bool {
for _, changeset := range s.ChangeSets {
if f(changeset) == false {
return false
}
}
return true
}
func (s *Serie) String() string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Serie[%d]", len(s.ChangeSets)))
if len(s.ChangeSets) == 0 {
sb.WriteString("()\n")
return sb.String()
}
parentCommitIDs, err := s.GetParentCommitIDs()
if err == nil {
if len(parentCommitIDs) == 1 {
sb.WriteString(fmt.Sprintf("(parent: %.7s)", parentCommitIDs[0]))
} else {
sb.WriteString("(merge: ")
for i, parentCommitID := range parentCommitIDs {
sb.WriteString(fmt.Sprintf("%.7s", parentCommitID))
if i < len(parentCommitIDs) {
sb.WriteString(", ")
}
}
sb.WriteString(")")
}
}
sb.WriteString(fmt.Sprintf("(%.7s..%.7s)",
s.ChangeSets[0].CommitID,
s.ChangeSets[len(s.ChangeSets)-1].CommitID))
return sb.String()
}
func shortCommitID(commitID string) string {
return commitID[:6]
}

View file

@ -1,126 +0,0 @@
package gerrit
import (
"sort"
"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 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, logger *log.Logger) ([]*Serie, error) {
series := make([]*Serie, 0)
mapLeafToSerie := make(map[string]*Serie, 0)
for _, changeset := range changesets {
l := logger.WithField("changeset", changeset.String())
l.Debug("creating initial serie")
serie := &Serie{
ChangeSets: []*Changeset{changeset},
}
series = append(series, serie)
mapLeafToSerie[changeset.CommitID] = serie
}
// Combine series using a fixpoint approach, with a max iteration count.
logger.Debug("glueing together phase")
for i := 1; i < 100; i++ {
didUpdate := false
logger.Debugf("at iteration %d", i)
for j, serie := range series {
l := logger.WithFields(log.Fields{
"i": i,
"j": j,
"serie": serie.String(),
})
parentCommitIDs, err := serie.GetParentCommitIDs()
if err != nil {
return series, err
}
if len(parentCommitIDs) != 1 {
// We can't append merge commits to other series
l.Infof("No single parent, skipping.")
continue
}
parentCommitID := parentCommitIDs[0]
l.Debug("Looking for a predecessor.")
// if there's another serie that has this parent as a leaf, glue together
if otherSerie, ok := mapLeafToSerie[parentCommitID]; ok {
if otherSerie == serie {
continue
}
l = l.WithField("otherSerie", otherSerie)
myLeafCommitID, err := serie.GetLeafCommitID()
if err != nil {
return series, err
}
// append our changesets to the other serie
l.Debug("Splicing together.")
otherSerie.ChangeSets = append(otherSerie.ChangeSets, serie.ChangeSets...)
delete(mapLeafToSerie, parentCommitID)
mapLeafToSerie[myLeafCommitID] = otherSerie
// orphan our serie
serie.ChangeSets = []*Changeset{}
// remove the orphaned serie from the lookup table
delete(mapLeafToSerie, myLeafCommitID)
didUpdate = true
} else {
l.Debug("Not found.")
}
}
series = removeOrphanedSeries(series)
if !didUpdate {
logger.Infof("converged after %d iterations", i)
break
}
}
// Check integrity, just to be on the safe side.
for _, serie := range series {
l := logger.WithField("serie", serie.String())
l.Debugf("checking integrity")
err := serie.CheckIntegrity()
if err != nil {
l.Errorf("checking integrity failed: %s", err)
}
}
return series, nil
}
// removeOrphanedSeries removes all empty series (that contain zero changesets)
func removeOrphanedSeries(series []*Serie) []*Serie {
newSeries := []*Serie{}
for _, serie := range series {
if len(serie.ChangeSets) != 0 {
newSeries = append(newSeries, serie)
}
}
return newSeries
}
// SortSeries sorts a list of series by the number of changesets in each serie, descending
func SortSeries(series []*Serie) []*Serie {
newSeries := make([]*Serie, len(series))
copy(newSeries, series)
sort.Slice(newSeries, func(i, j int) bool {
// the weight depends on the amount of changesets series changeset size
return len(series[i].ChangeSets) > len(series[j].ChangeSets)
})
return newSeries
}

View file

@ -1,10 +0,0 @@
module github.com/tweag/gerrit-queue
go 1.16
require (
github.com/andygrunwald/go-gerrit v0.0.0-20190825170856-5959a9bf9ff8
github.com/apex/log v1.1.1
github.com/google/go-querystring v1.0.0 // indirect
github.com/urfave/cli v1.22.1
)

View file

@ -1,69 +0,0 @@
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 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/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/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/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/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.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/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/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/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/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-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-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/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

@ -1,137 +0,0 @@
package main
import (
"os"
"time"
"net/http"
"github.com/tweag/gerrit-queue/frontend"
"github.com/tweag/gerrit-queue/gerrit"
"github.com/tweag/gerrit-queue/misc"
"github.com/tweag/gerrit-queue/submitqueue"
"github.com/urfave/cli"
"github.com/apex/log"
"github.com/apex/log/handlers/multi"
"github.com/apex/log/handlers/text"
)
func main() {
var URL, username, password, projectName, branchName string
var fetchOnly bool
var triggerInterval int
app := cli.NewApp()
app.Name = "gerrit-queue"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "url",
Usage: "URL to the gerrit instance",
EnvVar: "GERRIT_URL",
Destination: &URL,
Required: true,
},
cli.StringFlag{
Name: "username",
Usage: "Username to use to login to gerrit",
EnvVar: "GERRIT_USERNAME",
Destination: &username,
Required: true,
},
cli.StringFlag{
Name: "password",
Usage: "Password to use to login to gerrit",
EnvVar: "GERRIT_PASSWORD",
Destination: &password,
Required: true,
},
cli.StringFlag{
Name: "project",
Usage: "Gerrit project name to run the submit queue for",
EnvVar: "GERRIT_PROJECT",
Destination: &projectName,
Required: true,
},
cli.StringFlag{
Name: "branch",
Usage: "Destination branch",
EnvVar: "GERRIT_BRANCH",
Destination: &branchName,
Value: "master",
},
cli.IntFlag{
Name: "trigger-interval",
Usage: "How often we should trigger ourselves (interval in seconds)",
EnvVar: "SUBMIT_QUEUE_TRIGGER_INTERVAL",
Destination: &triggerInterval,
Value: 600,
},
cli.BoolFlag{
Name: "fetch-only",
Usage: "Only fetch changes and assemble queue, but don't actually write",
EnvVar: "SUBMIT_QUEUE_FETCH_ONLY",
Destination: &fetchOnly,
},
}
rotatingLogHandler := misc.NewRotatingLogHandler(10000)
l := &log.Logger{
Handler: multi.New(
text.New(os.Stderr),
rotatingLogHandler,
),
Level: log.DebugLevel,
}
app.Action = func(c *cli.Context) error {
gerrit, err := gerrit.NewClient(l, URL, username, password, projectName, branchName)
if err != nil {
return err
}
log.Infof("Successfully connected to gerrit at %s", URL)
runner := submitqueue.NewRunner(l, gerrit)
handler := frontend.MakeFrontend(rotatingLogHandler, gerrit, runner)
// fetch only on first run
err = runner.Trigger(fetchOnly)
if err != nil {
log.Error(err.Error())
}
// ticker
go func() {
for {
time.Sleep(time.Duration(triggerInterval) * time.Second)
err = runner.Trigger(fetchOnly)
if err != nil {
log.Error(err.Error())
}
}
}()
server := http.Server{
Addr: ":8080",
Handler: handler,
}
server.ListenAndServe()
if err != nil {
log.Fatalf(err.Error())
}
return nil
}
err := app.Run(os.Args)
if err != nil {
log.Fatal(err.Error())
}
// TODOS:
// - handle event log, either by accepting webhooks, or by streaming events?
}

View file

@ -1,34 +0,0 @@
package misc
import (
"sync"
"github.com/apex/log"
)
// RotatingLogHandler implementation.
type RotatingLogHandler struct {
mu sync.Mutex
Entries []*log.Entry
maxEntries int
}
// NewRotatingLogHandler creates a new rotating log handler
func NewRotatingLogHandler(maxEntries int) *RotatingLogHandler {
return &RotatingLogHandler{
maxEntries: maxEntries,
}
}
// HandleLog implements log.Handler.
func (h *RotatingLogHandler) HandleLog(e *log.Entry) error {
h.mu.Lock()
defer h.mu.Unlock()
// drop tail if we have more entries than maxEntries
if len(h.Entries) > h.maxEntries {
h.Entries = append([]*log.Entry{e}, h.Entries[:(h.maxEntries-2)]...)
} else {
h.Entries = append([]*log.Entry{e}, h.Entries...)
}
return nil
}

View file

@ -1,220 +0,0 @@
package submitqueue
import (
"fmt"
"sync"
"github.com/apex/log"
"github.com/tweag/gerrit-queue/gerrit"
)
// 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
currentlyRunning bool
wipSerie *gerrit.Serie
logger *log.Logger
gerrit *gerrit.Client
}
// NewRunner creates a new Runner struct
func NewRunner(logger *log.Logger, gerrit *gerrit.Client) *Runner {
return &Runner{
logger: logger,
gerrit: gerrit,
}
}
// isAutoSubmittable determines if something could be autosubmitted, potentially requiring a rebase
// for this, it needs to:
// * have the "Autosubmit" label set to +1
// * have gerrit's 'submittable' field set to true
// it doesn't check if the series is rebased on HEAD
func (r *Runner) isAutoSubmittable(s *gerrit.Serie) bool {
for _, c := range s.ChangeSets {
if c.Submittable != true || !c.IsAutosubmit() {
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 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")
}
r.currentlyRunning = true
r.mut.Unlock()
defer func() {
r.mut.Lock()
r.currentlyRunning = false
r.mut.Unlock()
}()
// Prepare the work by creating a local cache of gerrit state
err := r.gerrit.Refresh()
if err != nil {
return err
}
// 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")
// discard wipSerie not rebased on HEAD
// we rebase them at the end of the loop, so this means master advanced without going through the submit queue
if !r.gerrit.SerieIsRebasedOnHEAD(r.wipSerie) {
l.Warnf("HEAD has moved to %v while still waiting for wipSerie, discarding it", r.gerrit.GetHEAD())
r.wipSerie = nil
continue
}
// we now need to check CI feedback:
// wipSerie might have failed CI in the meantime
for _, c := range r.wipSerie.ChangeSets {
if c == nil {
l.Error("BUG: changeset is nil")
continue
}
if c.Verified < 0 {
l.WithField("failingChangeset", c).Warnf("wipSerie failed CI in the meantime, discarding.")
r.wipSerie = nil
continue
}
}
// it might still be waiting for CI
for _, c := range r.wipSerie.ChangeSets {
if c == nil {
l.Error("BUG: changeset is nil")
continue
}
if c.Verified == 0 {
l.WithField("pendingChangeset", c).Warnf("still waiting for CI feedback in wipSerie, going back to sleep.")
// break the loop, take a look at it at the next trigger.
return nil
}
}
// it might be autosubmittable
if r.isAutoSubmittable(r.wipSerie) {
l.Infof("submitting 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 {
l.Error("BUG: wipSerie is not autosubmittable")
r.wipSerie = nil
}
}
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(func(s *gerrit.Serie) bool {
return r.isAutoSubmittable(s) && s.ChangeSets[0].ParentCommitIDs[0] == r.gerrit.GetHEAD()
})
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("no more submittable series found, 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
}
// we don't need to care about updating the rebased changesets or getting the updated HEAD,
// as 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

@ -138,4 +138,23 @@ depot.nix.readTree.drvTargets {
./patches/evans-add-support-for-unix-domain-sockets.patch
];
});
# Package gerrit-queue, which is not in nixpkgs yet
gerrit-queue = super.buildGoModule {
pname = "gerrit-queue";
version = "unstable-2023-10-20";
vendorHash = "sha256-+Ig4D46NphzpWKXO23Haea9EqVtpda8v9zLPJkbe3bQ=";
src = super.fetchFromGitHub {
owner = "flokli";
repo = "gerrit-queue";
rev = "0186dbde15c9b11dc17b422feb74c842f6fa605a";
hash = "sha256-zXB5vre/Vr7UOyeMnf2RCtMKm+v5RENH7kGPr/2o7mI=";
};
meta = with lib; {
description = "Gerrit submit bot";
homepage = "https://github.com/tweag/gerrit-queue";
license = licenses.asl20;
};
};
}