Implement sticky approvals with copyMaxScore

Labels that have copyMaxScore set true are automatically copied to the
new patch set. This permits projects to enable Code-Review+2 to always
move forward, avoiding the need to re-review trivial fixups.

Change-Id: Ib5f1acb69937cb61897d79cb9a966744fa57b9c1
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 40c380c..3d0f827 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -219,6 +219,14 @@
 If true, the lowest possible negative value for the label is copied
 forward when a new patch set is uploaded.
 
+[[label_copyMaxScore]]
+`label.Label-Name.copyMaxScore`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If true, the highest possible positive value for the label is copied
+forward when a new patch set is uploaded. This can be used to enable
+sticky approvals, reducing turn-around for trivial cleanups prior to
+submitting a change.
 
 [[label_canOverride]]
 `label.Label-Name.canOverride`
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
index 2049887..8a9a508 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
@@ -96,6 +96,7 @@
   protected String abbreviatedName;
   protected String functionName;
   protected boolean copyMinScore;
+  protected boolean copyMaxScore;
 
   protected List<LabelValue> values;
   protected short maxNegative;
@@ -187,6 +188,14 @@
     this.copyMinScore = copyMinScore;
   }
 
+  public boolean isCopyMaxScore() {
+    return copyMaxScore;
+  }
+
+  public void setCopyMaxScore(boolean copyMaxScore) {
+    this.copyMaxScore = copyMaxScore;
+  }
+
   public boolean isMaxNegative(PatchSetApproval ca) {
     return maxNegative == ca.getValue();
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
index 5b064c8..ba50417 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
@@ -22,15 +22,14 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mail.CommitMessageEditedSender;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.mail.CommitMessageEditedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -76,7 +75,6 @@
   private final PatchSetInfoFactory patchSetInfoFactory;
 
   private final PersonIdent myIdent;
-  private final ApprovalsUtil approvalsUtil;
   private final TrackingFooters trackingFooters;
 
   @Inject
@@ -91,7 +89,7 @@
       final PatchSetInfoFactory patchSetInfoFactory,
       final GitReferenceUpdated gitRefUpdated,
       @GerritPersonIdent final PersonIdent myIdent,
-      final ApprovalsUtil approvalsUtil, TrackingFooters trackingFooters) {
+      TrackingFooters trackingFooters) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.currentUser = currentUser;
@@ -107,7 +105,6 @@
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.gitRefUpdated = gitRefUpdated;
     this.myIdent = myIdent;
-    this.approvalsUtil = approvalsUtil;
     this.trackingFooters = trackingFooters;
   }
 
@@ -136,7 +133,7 @@
 
       ChangeUtil.editCommitMessage(patchSetId, control.getRefControl(), commitValidators, currentUser, message, db,
           commitMessageEditedSenderFactory, hooks, git, patchSetInfoFactory, gitRefUpdated, myIdent,
-          approvalsUtil, trackingFooters);
+          trackingFooters);
 
       return changeDetailFactory.create(changeId).call();
     } finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 0ce6892..76c33ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.Id;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -30,7 +29,6 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -69,36 +67,30 @@
    * Moves the PatchSetApprovals to the specified PatchSet on the change from
    * the prior PatchSet, while keeping the vetos.
    *
-   * @param db database connection to use for updates.
-   * @param dest PatchSet to copy to
    * @throws OrmException
    * @return List<PatchSetApproval> The previous approvals
    */
-  public List<PatchSetApproval> copyVetosToPatchSet(ReviewDb db,
-      LabelTypes labelTypes, PatchSet.Id dest) throws OrmException {
-    PatchSet.Id source;
-    if (dest.get() > 1) {
-      source = new PatchSet.Id(dest.getParentKey(), dest.get() - 1);
-    } else {
-      throw new OrmException("Previous patch set could not be found");
-    }
-
-    List<PatchSetApproval> patchSetApprovals =
-        db.patchSetApprovals().byChange(dest.getParentKey()).toList();
-    for (PatchSetApproval a : patchSetApprovals) {
+  public static List<PatchSetApproval> copyLabels(ReviewDb db,
+      LabelTypes labelTypes,
+      PatchSet.Id source,
+      PatchSet.Id dest) throws OrmException {
+    List<PatchSetApproval> copied = Lists.newArrayList();
+    for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(source)) {
       LabelType type = labelTypes.byLabel(a.getLabelId());
-      if (type != null && a.getPatchSetId().equals(source) &&
-          type.isCopyMinScore() &&
-          type.isMaxNegative(a)) {
-        db.patchSetApprovals().insert(
-            Collections.singleton(new PatchSetApproval(dest, a)));
+      if (type == null) {
+        continue;
+      } else if (type.isCopyMinScore() && type.isMaxNegative(a)) {
+        copied.add(new PatchSetApproval(dest, a));
+      } else if (type.isCopyMaxScore() && type.isMaxPositive(a)) {
+        copied.add(new PatchSetApproval(dest, a));
       }
     }
-    return patchSetApprovals;
+    db.patchSetApprovals().insert(copied);
+    return copied;
   }
 
   public void addReviewers(ReviewDb db, LabelTypes labelTypes, Change change,
-      PatchSet ps, PatchSetInfo info, Set<Id> wantReviewers,
+      PatchSet ps, PatchSetInfo info, Set<Account.Id> wantReviewers,
       Set<Account.Id> existingReviewers) throws OrmException {
     List<LabelType> allTypes = labelTypes.getLabelTypes();
     if (allTypes.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 41a97d2..a1dbd54 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -330,7 +330,7 @@
       final ChangeHooks hooks, Repository git,
       final PatchSetInfoFactory patchSetInfoFactory,
       final GitReferenceUpdated gitRefUpdated, PersonIdent myIdent,
-      final ApprovalsUtil approvalsUtil, final TrackingFooters trackingFooters)
+      final TrackingFooters trackingFooters)
       throws NoSuchChangeException, EmailException, OrmException,
       MissingObjectException, IncorrectObjectTypeException, IOException,
       InvalidChangeOperationException, PatchSetInfoNotAvailableException {
@@ -445,8 +445,9 @@
               "Change %s was modified", change.getId()));
         }
 
-        approvalsUtil.copyVetosToPatchSet(db,
+        ApprovalsUtil.copyLabels(db,
             refControl.getProjectControl().getLabelTypes(),
+            originalPS.getId(),
             change.currentPatchSetId());
 
         final List<FooterLine> footerLines = newCommit.getFooterLines();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index 6ae8c96..e5f6b8f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -72,7 +71,6 @@
   private final GitReferenceUpdated gitRefUpdated;
   private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
   private final ChangeHookRunner hooks;
-  private final ApprovalsUtil approvalsUtil;
   private final MergeUtil.Factory mergeUtilFactory;
 
   @Inject
@@ -82,7 +80,7 @@
       final GitRepositoryManager gitManager,
       final GitReferenceUpdated gitRefUpdated,
       final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
-      final ChangeHookRunner hooks, final ApprovalsUtil approvalsUtil,
+      final ChangeHookRunner hooks,
       final MergeUtil.Factory mergeUtilFactory) {
     this.changeControlFactory = changeControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
@@ -92,7 +90,6 @@
     this.gitRefUpdated = gitRefUpdated;
     this.rebasedPatchSetSenderFactory = rebasedPatchSetSenderFactory;
     this.hooks = hooks;
-    this.approvalsUtil = approvalsUtil;
     this.mergeUtilFactory = mergeUtilFactory;
   }
 
@@ -377,9 +374,9 @@
             "Change %s was modified", change.getId()));
       }
 
-      final LabelTypes labelTypes = changeControlFactory.controlFor(change)
-          .getLabelTypes();
-      approvalsUtil.copyVetosToPatchSet(db, labelTypes,
+      ApprovalsUtil.copyLabels(db,
+          changeControlFactory.controlFor(change).getLabelTypes(),
+          patchSetId,
           change.currentPatchSetId());
 
       final ChangeMessage cmsg =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 2a260e4d..852b5e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -112,6 +112,7 @@
   private static final String KEY_ABBREVIATION = "abbreviation";
   private static final String KEY_FUNCTION = "function";
   private static final String KEY_COPY_MIN_SCORE = "copyMinScore";
+  private static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
   private static final String KEY_VALUE = "value";
   private static final String KEY_CAN_OVERRIDE = "canOverride";
   private static final Set<String> LABEL_FUNCTIONS = ImmutableSet.of(
@@ -581,6 +582,8 @@
       }
       label.setCopyMinScore(
           rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, false));
+      label.setCopyMaxScore(
+          rc.getBoolean(LABEL, name, KEY_COPY_MAX_SCORE, false));
       label.setCanOverride(
           rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, true));
       labelSections.put(name, label);
@@ -848,6 +851,11 @@
       } else {
         rc.unset(LABEL, name, KEY_COPY_MIN_SCORE);
       }
+      if (label.isCopyMaxScore()) {
+        rc.setBoolean(LABEL, name, KEY_COPY_MAX_SCORE, true);
+      } else {
+        rc.unset(LABEL, name, KEY_COPY_MAX_SCORE);
+      }
       if (!label.canOverride()) {
         rc.setBoolean(LABEL, name, KEY_CAN_OVERRIDE, false);
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 308f5db..9dd5a4f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -18,7 +18,6 @@
 import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
-
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
@@ -50,7 +49,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -1767,10 +1765,9 @@
           mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
         }
 
-        List<PatchSetApproval> patchSetApprovals =
-            approvalsUtil.copyVetosToPatchSet(db, labelTypes, newPatchSet.getId());
         final MailRecipients oldRecipients =
-            getRecipientsFromApprovals(patchSetApprovals);
+            getRecipientsFromApprovals(ApprovalsUtil.copyLabels(
+                db, labelTypes, priorPatchSet, newPatchSet.getId()));
         approvalsUtil.addReviewers(db, labelTypes, change, newPatchSet, info,
             recipients.getReviewers(), oldRecipients.getAll());
         recipients.add(oldRecipients);