Persist inherited labels locally in case they are modified

Before this commit, editing an inherited label would fail as only local
labels can be modified. This commit adds logic to copy down a label in
case it should be modified.

Change-Id: Icfa329bf7d6d6e43835c0fd94a407ea713161746
diff --git a/src/main/java/com/googlesource/gerrit/plugins/simplesubmitrules/config/ConfigTranslator.java b/src/main/java/com/googlesource/gerrit/plugins/simplesubmitrules/config/ConfigTranslator.java
index 035a2a8..de7783c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/simplesubmitrules/config/ConfigTranslator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/simplesubmitrules/config/ConfigTranslator.java
@@ -14,9 +14,9 @@
 
 package com.googlesource.gerrit.plugins.simplesubmitrules.config;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.server.config.PluginConfig;
@@ -108,15 +108,32 @@
   void applyTo(SubmitConfig inConfig, ProjectState projectState) throws BadRequestException {
     PluginConfig pluginConfig = pluginConfigFactory.getFromProjectConfig(projectState, pluginName);
     applyCommentRulesTo(inConfig.comments, pluginConfig);
-    applyLabelsTo(inConfig.labels, projectState.getLabelTypes());
+    applyLabelsTo(inConfig.labels, projectState);
   }
 
-  private static void applyLabelsTo(Map<String, LabelDefinition> labels, LabelTypes labelTypes)
+  private static void applyLabelsTo(Map<String, LabelDefinition> labels, ProjectState projectState)
       throws BadRequestException {
+    if (labels.isEmpty()) {
+      return;
+    }
+
     for (Map.Entry<String, LabelDefinition> entry : labels.entrySet()) {
+      if (!projectState.getConfig().getLabelSections().containsKey(entry.getKey())) {
+        // The current project does not have this label. Try to copy it down from the inherited
+        // labels to be able to modify it locally.
+        Map<String, LabelType> copiedLabelTypes = projectState.getConfig().getLabelSections();
+        projectState
+            .getLabelTypes()
+            .getLabelTypes()
+            .stream()
+            .filter(l -> l.getName().equals(entry.getKey()))
+            .filter(l -> l.canOverride())
+            .forEach(l -> copiedLabelTypes.put(l.getName(), copyLabelType(l)));
+      }
+
       String label = entry.getKey();
       LabelDefinition definition = entry.getValue();
-      LabelType labelType = labelTypes.byLabel(label);
+      LabelType labelType = projectState.getConfig().getLabelSections().get(label);
 
       if (labelType == null) {
         throw new BadRequestException(
@@ -152,4 +169,23 @@
         SimpleSubmitRulesConfig.KEY_BLOCK_IF_UNRESOLVED_COMMENTS,
         comments.blockIfUnresolvedComments);
   }
+
+  private static LabelType copyLabelType(LabelType label) {
+    // TODO(hiesel) Move this to core
+    LabelType copy = new LabelType(label.getName(), ImmutableList.copyOf(label.getValues()));
+    if (label.getRefPatterns() != null) {
+      copy.setRefPatterns(ImmutableList.copyOf(label.getRefPatterns()));
+    }
+    copy.setAllowPostSubmit(label.allowPostSubmit());
+    copy.setCanOverride(label.canOverride());
+    copy.setCopyAllScoresIfNoChange(label.isCopyAllScoresIfNoChange());
+    copy.setCopyAllScoresIfNoCodeChange(label.isCopyAllScoresIfNoCodeChange());
+    copy.setCopyAllScoresOnMergeFirstParentUpdate(label.isCopyAllScoresOnMergeFirstParentUpdate());
+    copy.setCopyAllScoresOnTrivialRebase(label.isCopyAllScoresOnTrivialRebase());
+    copy.setIgnoreSelfApproval(label.ignoreSelfApproval());
+    copy.setCopyMaxScore(label.isCopyMaxScore());
+    copy.setCopyMinScore(label.isCopyMinScore());
+    copy.setFunction(label.getFunction());
+    return copy;
+  }
 }
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 15c0e18..3305d79 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -53,8 +53,10 @@
 The comments section defines the rules to apply to comments (can the change be submitted with
 unresolved comments, …). The labels section defines each label.
 
-It is reasonable to consider that a label missing from the labels section won't be reset, but
-consumers should not rely upon it.
+When reading labels on the API, the result includes both local and inherited labels.
+When the configuration is modified through the API, the plugin will check if there are
+local label configurations. If the request modifies an inherited label, it will be copied
+down so that it can be modified locally.
 
 ### CommentsRules
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/simplesubmitrules/PluginIT.java b/src/test/java/com/googlesource/gerrit/plugins/simplesubmitrules/PluginIT.java
index d41ed1c..2ddbba7 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/simplesubmitrules/PluginIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/simplesubmitrules/PluginIT.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.Side;
@@ -37,7 +38,6 @@
 import com.googlesource.gerrit.plugins.simplesubmitrules.api.SubmitConfig;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
-import org.junit.Before;
 import org.junit.Test;
 
 @TestPlugin(
@@ -47,16 +47,6 @@
 public class PluginIT extends LightweightPluginDaemonTest {
   private static final String JSON_TYPE = "application/json";
 
-  @Before
-  public void setUpCodeReviewLabel() throws Exception {
-    // TODO(hiesel): Remove once copy-down logic is in place
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType codeReview = projectCache.getAllProjects().getLabelTypes().byLabel("Code-Review");
-      u.getConfig().getLabelSections().put("Code-Review", codeReview);
-      u.save();
-    }
-  }
-
   @Test
   public void singleApprovalIsSufficientByDefault() throws Exception {
     PushOneCommit.Result r = createChange();
@@ -145,6 +135,16 @@
     assertThat(parsedConfig.comments).isEqualTo(new CommentsRules(true));
   }
 
+  @Test
+  public void pluginPersistsLabelInCurrentProjectWhenOverrideIsNeeded() throws Exception {
+    LabelDefinition codeReview = new LabelDefinition("MaxNoBlock", false, null);
+    SubmitConfig config = new SubmitConfig(ImmutableMap.of("Code-Review", codeReview), null);
+    postConfig(project, config);
+
+    LabelType myLabel = projectCache.get(project).getConfig().getLabelSections().get("Code-Review");
+    assertThat(myLabel.getFunction()).isEqualTo(LabelFunction.MAX_NO_BLOCK);
+  }
+
   private void postConfig(Project.NameKey project, SubmitConfig config) throws Exception {
     RawInput rawInput =
         RawInputUtil.create(newGson().toJson(config).getBytes(Charsets.UTF_8), JSON_TYPE);