subtree(3p/gerrit-queue): Vendor at commit '24f5a642'
Imported from github/tvlfyi/gerrit-queue, originally from github/tweag/gerrit-queue but that upstream is unmaintained. git-subtree-dir: third_party/gerrit-queue git-subtree-mainline:ff10b7ab83
git-subtree-split:24f5a642af
Change-Id: I307cc38185ab9e25eb102c95096298a150ae13a2
This commit is contained in:
commit
59f97332b3
21 changed files with 1641 additions and 0 deletions
4
third_party/gerrit-queue/.buildkite/build.sh
vendored
Executable file
4
third_party/gerrit-queue/.buildkite/build.sh
vendored
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/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
|
13
third_party/gerrit-queue/.buildkite/pipeline.yml
vendored
Normal file
13
third_party/gerrit-queue/.buildkite/pipeline.yml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
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/*"
|
17
third_party/gerrit-queue/.envrc
vendored
Normal file
17
third_party/gerrit-queue/.envrc
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
# This configures [direnv](https://direnv.net/) if installed and enabled to
|
||||
# automatically enter the nix-shell defined in `shell.nix`,
|
||||
# either by using lorri if available, or nix-shell otherwise.
|
||||
|
||||
if type lorri &>/dev/null; then
|
||||
eval "$(lorri direnv)"
|
||||
else
|
||||
# fall back to using direnv's builtin nix support (blocking)
|
||||
use nix
|
||||
fi
|
||||
|
||||
# enable go modules
|
||||
export GO111MODULE=on
|
||||
unset GOPATH
|
||||
|
||||
# Load private overrides
|
||||
[[ -f .envrc.private ]] && source_env .envrc.private
|
4
third_party/gerrit-queue/.gitignore
vendored
Normal file
4
third_party/gerrit-queue/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/.vscode
|
||||
/statik
|
||||
/.envrc.private
|
||||
/gerrit-queue
|
201
third_party/gerrit-queue/LICENSE
vendored
Normal file
201
third_party/gerrit-queue/LICENSE
vendored
Normal file
|
@ -0,0 +1,201 @@
|
|||
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.
|
80
third_party/gerrit-queue/README.md
vendored
Normal file
80
third_party/gerrit-queue/README.md
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
# 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
|
||||
```
|
20
third_party/gerrit-queue/default.nix
vendored
Normal file
20
third_party/gerrit-queue/default.nix
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
{ pkgs, lib, ... }:
|
||||
|
||||
pkgs.buildGoModule {
|
||||
pname = "gerrit-queue";
|
||||
version = "master";
|
||||
vendorSha256 = "0hivr4yn9aa1vk7z1h1nwg75hzqnsaxypi1wwxdy1l1hnm5k8hhi";
|
||||
src = ./.;
|
||||
|
||||
# gerrit-queue embeds static assets which need to be generated
|
||||
nativeBuildInputs = [ pkgs.statik ];
|
||||
preBuild = ''
|
||||
statik -f
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "Gerrit submit bot";
|
||||
homepage = "https://github.com/tweag/gerrit-queue";
|
||||
license = licenses.asl20;
|
||||
};
|
||||
}
|
117
third_party/gerrit-queue/frontend/frontend.go
vendored
Normal file
117
third_party/gerrit-queue/frontend/frontend.go
vendored
Normal file
|
@ -0,0 +1,117 @@
|
|||
package frontend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
|
||||
"html/template"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rakyll/statik/fs"
|
||||
|
||||
"github.com/apex/log"
|
||||
|
||||
"github.com/tweag/gerrit-queue/gerrit"
|
||||
"github.com/tweag/gerrit-queue/misc"
|
||||
_ "github.com/tweag/gerrit-queue/statik" // register static assets
|
||||
"github.com/tweag/gerrit-queue/submitqueue"
|
||||
)
|
||||
|
||||
//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) {
|
||||
if len(templateNames) == 0 {
|
||||
return nil, fmt.Errorf("templateNames can't be empty")
|
||||
}
|
||||
tmpl := template.New(templateNames[0]).Funcs(funcMap)
|
||||
statikFS, err := fs.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, templateName := range templateNames {
|
||||
r, err := statikFS.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 {
|
||||
router := gin.Default()
|
||||
|
||||
projectName := gerritClient.GetProjectName()
|
||||
branchName := gerritClient.GetBranchName()
|
||||
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
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(c.Writer, "index.tmpl.html", gin.H{
|
||||
// Config
|
||||
"projectName": projectName,
|
||||
"branchName": branchName,
|
||||
|
||||
// State
|
||||
"currentlyRunning": currentlyRunning,
|
||||
"wipSerie": wipSerie,
|
||||
"HEAD": HEAD,
|
||||
|
||||
// History
|
||||
"memory": rotatingLogHandler,
|
||||
})
|
||||
})
|
||||
return router
|
||||
}
|
117
third_party/gerrit-queue/gerrit/changeset.go
vendored
Normal file
117
third_party/gerrit-queue/gerrit/changeset.go
vendored
Normal file
|
@ -0,0 +1,117 @@
|
|||
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
|
||||
}
|
220
third_party/gerrit-queue/gerrit/client.go
vendored
Normal file
220
third_party/gerrit-queue/gerrit/client.go
vendored
Normal file
|
@ -0,0 +1,220 @@
|
|||
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
|
||||
}
|
112
third_party/gerrit-queue/gerrit/serie.go
vendored
Normal file
112
third_party/gerrit-queue/gerrit/serie.go
vendored
Normal file
|
@ -0,0 +1,112 @@
|
|||
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]
|
||||
}
|
126
third_party/gerrit-queue/gerrit/series.go
vendored
Normal file
126
third_party/gerrit-queue/gerrit/series.go
vendored
Normal file
|
@ -0,0 +1,126 @@
|
|||
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
|
||||
}
|
12
third_party/gerrit-queue/go.mod
vendored
Normal file
12
third_party/gerrit-queue/go.mod
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
module github.com/tweag/gerrit-queue
|
||||
|
||||
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/urfave/cli v1.22.1
|
||||
)
|
91
third_party/gerrit-queue/go.sum
vendored
Normal file
91
third_party/gerrit-queue/go.sum
vendored
Normal file
|
@ -0,0 +1,91 @@
|
|||
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/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/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/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/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/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/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/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 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
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 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=
|
139
third_party/gerrit-queue/main.go
vendored
Normal file
139
third_party/gerrit-queue/main.go
vendored
Normal file
|
@ -0,0 +1,139 @@
|
|||
//go:generate statik -f
|
||||
|
||||
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?
|
||||
}
|
34
third_party/gerrit-queue/misc/rotatingloghandler.go
vendored
Normal file
34
third_party/gerrit-queue/misc/rotatingloghandler.go
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
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
|
||||
}
|
15
third_party/gerrit-queue/public/changeset.tmpl.html
vendored
Normal file
15
third_party/gerrit-queue/public/changeset.tmpl.html
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
{{ 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 }}
|
76
third_party/gerrit-queue/public/index.tmpl.html
vendored
Normal file
76
third_party/gerrit-queue/public/index.tmpl.html
vendored
Normal file
|
@ -0,0 +1,76 @@
|
|||
<!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>
|
19
third_party/gerrit-queue/public/serie.tmpl.html
vendored
Normal file
19
third_party/gerrit-queue/public/serie.tmpl.html
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
{{ 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 }}
|
12
third_party/gerrit-queue/shell.nix
vendored
Normal file
12
third_party/gerrit-queue/shell.nix
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
let
|
||||
pkgs = (import (builtins.fetchTarball {
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/5de728659b412bcf7d18316a4b71d9a6e447f460.tar.gz";
|
||||
sha256 = "1bdykda8k8gl2vcp36g27xf3437ig098yrhjp0hclv7sn6dp2w1l";
|
||||
})) {};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
buildInputs = [
|
||||
pkgs.go_1_12
|
||||
pkgs.statik
|
||||
];
|
||||
}
|
212
third_party/gerrit-queue/submitqueue/runner.go
vendored
Normal file
212
third_party/gerrit-queue/submitqueue/runner.go
vendored
Normal file
|
@ -0,0 +1,212 @@
|
|||
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 {} 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.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.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 {
|
||||
// should never be reached?!
|
||||
log.Warnf("reached branch we should never reach")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
Loading…
Reference in a new issue