Add utilities to talk to Gerrit

This prepare the move of the gerrit-verifier job to a pipeline job

Issue #12

Change-Id: I11e5b63f66318e8ebed7cb770d85db6b5e198d67
diff --git a/jenkins/lib/src/build/bazel/ci/GerritUtils.groovy b/jenkins/lib/src/build/bazel/ci/GerritUtils.groovy
new file mode 100644
index 0000000..6bba64d
--- /dev/null
+++ b/jenkins/lib/src/build/bazel/ci/GerritUtils.groovy
@@ -0,0 +1,194 @@
+// Copyright (C) 2017 The Bazel Authors
+//
+// 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.
+
+// Inspired by https://gerrit.googlesource.com/gerrit-ci-scripts/+/master/jenkins/gerrit-verifier-flow.groovy
+package build.bazel.ci
+
+import groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+
+/*
+ * This is a class to communicate with Gerrit (basically add code reviews).
+ *
+ * All methods to talk to gerrit are NonCPS to avoid serialization issues from Jenkins
+ * and make them atomic.
+ */
+class GerritUtils implements java.io.Serializable {
+  private String server
+  private String cookies
+  private String reviewer
+  private String reviewerEmail
+
+  // Parse a cookie file from cURL, ignoring most of the field
+  @NonCPS
+  private static def loadCookiesFile(String host, String cookieFile) {
+    def url = new URI(host)
+    host = url.host
+    try {
+      String[] fileContent = new File(cookieFile).text.split("\n")
+      def result = []
+      for (line in fileContent) {
+        if (!line.startsWith("#") && !line.isEmpty()) {
+          def elements = line.split("\t")
+          if (elements.length > 0 && host.endsWith(elements[0])) {
+            result << "${elements[5]}=${elements[6]}"
+          }
+        }
+      }
+      return result.join("; ")
+    } catch(IOException exn) {
+      return []
+    }
+  }
+
+  // Initialize the utilities to connect to ${server} using cookies
+  // from ${cookiesFile} and user ${reviewer} as the bot user.
+  def GerritUtils(String server, String cookiesFile, String reviewer) {
+    this.server = server
+    if (!this.server.endsWith("/")) {
+      this.server += "/"
+    }
+    this.cookies = loadCookiesFile(server, cookiesFile)
+    this.reviewer = reviewer
+    def m = reviewer =~ /^.*<([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)>$/
+    if (!m) {
+      // This is fine to use non checked exception since it's for bubbling up
+      // to jenkins
+      throw new Exception(
+          "Reviewer argument does not match the pattern 'Name <email@domain>'")
+    }
+    this.reviewerEmail = m[0][1]
+  }
+
+  // Getters
+  def getServer() {
+    return server
+  }
+
+  def getCookies() {
+    return cookies
+  }
+
+  def getReviewer() {
+    return reviewer
+  }
+
+  def getReviewerEmail() {
+    return reviewerEmail
+  }
+
+  // Return the URL of a change
+  def url(changeNum, patchNum = 0) {
+    return patchNum ? "${this.server}#/c/${changeNum}/${patchNum}" : "${this.server}#/c/${changeNum}"
+  }
+
+  // Post a JSON payload to the given url setting the correct cookies for authentication.
+  @NonCPS
+  private def post(path, data) {
+    def payload = JsonOutput.toJson(data)
+    def url = new URL(this.server + path)
+    URLConnection con = url.openConnection()
+    con.setDoOutput(true)
+    con.setRequestMethod("POST")
+    con.setRequestProperty("Cookie", cookies)
+    con.setRequestProperty("Content-Type", "application/json")
+    def wr = con.getOutputStream()
+    wr.write(payload.getBytes())
+    wr.flush()
+    wr.close()
+
+    int responseCode = con.getResponseCode()
+    return responseCode == 200
+  }
+
+  // Add the Gerrit bot as reviewer to change ${changeNum}
+  @NonCPS
+  def addReviewer(changeNum) {
+    return post("a/changes/${changeNum}/reviewers", [reviewer: this.reviewer])
+  }
+
+  // Set a change to verified +1/-1
+  // Parameters:
+  //   changeNum: number of the change to mark as verified
+  //   sha1: SHA-1 sum of the commit to mark as verified
+  //   verified: +1/-1 value for the verified flag on Gerrit
+  @NonCPS
+  def review(changeNum, sha1, verified, message = null) {
+    def payload = [
+      labels: ["Code-Review": 0, "Verified": verified],
+      notify: (verified < 0 ? "OWNER" : "NONE")
+    ]
+    if (message != null) {
+      payload["message"] = message
+    }
+    return post("a/changes/${changeNum}/revisions/${sha1}/review", payload)
+  }
+
+  // Add a comment without notifying everybody on the change.
+  //   buildUrl: URL of the build corresponding to that message
+  //   changeNum: change to comment on
+  //   sha1: SHA-1 of the commit to comment on
+  //   msgPrefix: the prefix of the message, the result message will be: <msgPrefix> Bazel CI: <buildUrl>
+  @NonCPS
+  def comment(changeNum, sha1, message) {
+    return post("a/changes/${changeNum}/revisions/${sha1}/review",
+                ["message": message, "notify": "NONE"])
+  }
+
+  // Query for gerrit for a list of change and return a list of
+  // changes as returned by the Gerrit API.
+  // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
+  // Args:
+  //   query: the query of changes
+  //   maxChanges: maximum number of changes to return, default to 0 means no maximum
+  // Returns:
+  //   An object translated from JSON as returned by the list-changes operation when asked
+  //   for DETAILED_LABELS and CURRENT_REVISION.
+  // Note: this method is mostly for used by GerritUtils itself.
+  @NonCPS
+  def query(query, maxChanges = 0) {
+    def url = server + "changes/?pp=0&o=DETAILED_LABELS&o=CURRENT_REVISION"
+    if (maxChanges > 0) {
+      url += "&n=${maxChanges}"
+    }
+    url += "&q=${java.net.URLEncoder.encode(query)}"
+    def changes = new URL(url).getText().substring(5)
+    def jsonSlurper = new JsonSlurper()
+    return jsonSlurper.parseText(changes)
+  }
+
+  // Returns the list of verified changes not reviewed by the Gerrit reviewer and matching
+  // the given filter. The result is a list of dictionnary of matching change, containing the
+  // sha1 of the last patch, the number of this patch, the number of the change, the reference
+  // of the patch and the project of the change.
+  @NonCPS
+  def getVerifiedChanges(filter = "", verifiedLevel = 1, maxChanges = 0) {
+    def changesJson = query("status:open -reviewer:${reviewerEmail} ${filter}",
+			    maxChanges).findAll { change ->
+        def verified = change.labels.Verified
+        return (verified != null) && verified.all.any({ it.value >= verifiedLevel })
+    }.collect {
+      it ->
+        def sha1 = it.current_revision
+        def patch = it.revisions.get(sha1)
+        return [
+          "sha1": sha1,
+          "number": it._number,
+          "patchNumber": patch._number,
+          "ref": patch.ref,
+          "project": it.project]
+    }
+    return changesJson
+  }
+}