Add REST endpoint to add new label definitions

Bug: Issue 11522
Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I09b8642d266feeeef0c5ae2bdeb106615e4f5bfe
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 65996a2..a09075d 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -3139,6 +3139,65 @@
   }
 ----
 
+[[create-label]]
+=== Create Label
+--
+'PUT /projects/link:#project-name[\{project-name\}]/labels/link:#label-name[\{label-name\}]'
+--
+
+Creates a new label definition in this project.
+
+The calling user must have write access to the `refs/meta/config` branch of the
+project.
+
+If a label with this name is already defined in this project, this label
+definition is updated (see link:#set-label[Set Label]).
+
+.Request
+----
+  PUT /projects/My-Project/labels/Foo HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "commit_message": "Create Foo Label",
+    "values": {
+      " 0": "No score",
+      "-1": "I would prefer this is not merged as is",
+      "-2": "This shall not be merged",
+      "+1": "Looks good to me, but someone else must approve",
+      "+2": "Looks good to me, approved"
+    }
+  }
+----
+
+As response a link:#label-definition-info[LabelDefinitionInfo] entity is
+returned that describes the created label.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "name": "Foo",
+    "project_name": "My-Project",
+    "function": "MaxWithBlock",
+    "values": {
+      " 0": "No score",
+      "-1": "I would prefer this is not merged as is",
+      "-2": "This shall not be merged",
+      "+1": "Looks good to me, but someone else must approve",
+      "+2": "Looks good to me, approved"
+    },
+    "default_value": 0,
+    "can_override": true,
+    "copy_all_scores_if_no_change": true,
+    "allow_post_submit": true
+  }
+----
+
 [[set-label]]
 === Set Label
 --
diff --git a/java/com/google/gerrit/extensions/api/projects/LabelApi.java b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
index f11d394..bee9e53 100644
--- a/java/com/google/gerrit/extensions/api/projects/LabelApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
@@ -20,6 +20,8 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 public interface LabelApi {
+  LabelApi create(LabelDefinitionInput input) throws RestApiException;
+
   LabelDefinitionInfo get() throws RestApiException;
 
   LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException;
@@ -30,6 +32,11 @@
    */
   class NotImplemented implements LabelApi {
     @Override
+    public LabelApi create(LabelDefinitionInput input) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public LabelDefinitionInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/server/api/projects/LabelApiImpl.java b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
index 4c8759e..887f5ae 100644
--- a/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
@@ -19,33 +19,70 @@
 import com.google.gerrit.extensions.api.projects.LabelApi;
 import com.google.gerrit.extensions.common.LabelDefinitionInfo;
 import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.LabelResource;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.CreateLabel;
 import com.google.gerrit.server.restapi.project.GetLabel;
+import com.google.gerrit.server.restapi.project.LabelsCollection;
 import com.google.gerrit.server.restapi.project.SetLabel;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 public class LabelApiImpl implements LabelApi {
   interface Factory {
-    LabelApiImpl create(LabelResource rsrc);
+    LabelApiImpl create(ProjectResource project, String label);
   }
 
+  private final LabelsCollection labels;
+  private final CreateLabel createLabel;
   private final GetLabel getLabel;
   private final SetLabel setLabel;
-  private final LabelResource rsrc;
+  private final ProjectCache projectCache;
+  private final String label;
+
+  private ProjectResource project;
 
   @Inject
-  LabelApiImpl(GetLabel getLabel, SetLabel setLabel, @Assisted LabelResource rsrc) {
+  LabelApiImpl(
+      LabelsCollection labels,
+      CreateLabel createLabel,
+      GetLabel getLabel,
+      SetLabel setLabel,
+      ProjectCache projectCache,
+      @Assisted ProjectResource project,
+      @Assisted String label) {
+    this.labels = labels;
+    this.createLabel = createLabel;
     this.getLabel = getLabel;
     this.setLabel = setLabel;
-    this.rsrc = rsrc;
+    this.projectCache = projectCache;
+    this.project = project;
+    this.label = label;
+  }
+
+  @Override
+  public LabelApi create(LabelDefinitionInput input) throws RestApiException {
+    try {
+      createLabel.apply(project, IdString.fromDecoded(label), input);
+
+      // recreate project resource because project state was updated by creating the new label and
+      // needs to be reloaded
+      project =
+          new ProjectResource(projectCache.checkedGet(project.getNameKey()), project.getUser());
+      return this;
+    } catch (Exception e) {
+      throw asRestApiException("Cannot create branch", e);
+    }
   }
 
   @Override
   public LabelDefinitionInfo get() throws RestApiException {
     try {
-      return getLabel.apply(rsrc).value();
+      return getLabel.apply(resource()).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot get label", e);
     }
@@ -54,9 +91,13 @@
   @Override
   public LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException {
     try {
-      return setLabel.apply(rsrc, input).value();
+      return setLabel.apply(resource(), input).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot update label", e);
     }
   }
+
+  private LabelResource resource() throws RestApiException, PermissionBackendException {
+    return labels.parse(project, IdString.fromDecoded(label));
+  }
 }
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index bce3b0a..d7ab91b 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -73,7 +73,6 @@
 import com.google.gerrit.server.restapi.project.GetParent;
 import com.google.gerrit.server.restapi.project.Index;
 import com.google.gerrit.server.restapi.project.IndexChanges;
-import com.google.gerrit.server.restapi.project.LabelsCollection;
 import com.google.gerrit.server.restapi.project.ListBranches;
 import com.google.gerrit.server.restapi.project.ListDashboards;
 import com.google.gerrit.server.restapi.project.ListLabels;
@@ -132,7 +131,6 @@
   private final Index index;
   private final IndexChanges indexChanges;
   private final Provider<ListLabels> listLabels;
-  private final LabelsCollection labels;
   private final LabelApiImpl.Factory labelApi;
 
   @AssistedInject
@@ -171,7 +169,6 @@
       IndexChanges indexChanges,
       Provider<ListLabels> listLabels,
       LabelApiImpl.Factory labelApi,
-      LabelsCollection labels,
       @Assisted ProjectResource project) {
     this(
         permissionBackend,
@@ -209,7 +206,6 @@
         indexChanges,
         listLabels,
         labelApi,
-        labels,
         null);
   }
 
@@ -249,7 +245,6 @@
       IndexChanges indexChanges,
       Provider<ListLabels> listLabels,
       LabelApiImpl.Factory labelApi,
-      LabelsCollection labels,
       @Assisted String name) {
     this(
         permissionBackend,
@@ -287,7 +282,6 @@
         indexChanges,
         listLabels,
         labelApi,
-        labels,
         name);
   }
 
@@ -327,7 +321,6 @@
       IndexChanges indexChanges,
       Provider<ListLabels> listLabels,
       LabelApiImpl.Factory labelApi,
-      LabelsCollection labels,
       String name) {
     this.permissionBackend = permissionBackend;
     this.createProject = createProject;
@@ -365,7 +358,6 @@
     this.indexChanges = indexChanges;
     this.listLabels = listLabels;
     this.labelApi = labelApi;
-    this.labels = labels;
   }
 
   @Override
@@ -715,7 +707,7 @@
   @Override
   public LabelApi label(String labelName) throws RestApiException {
     try {
-      return labelApi.create(labels.parse(checkExists(), IdString.fromDecoded(labelName)));
+      return labelApi.create(checkExists(), labelName);
     } catch (Exception e) {
       throw asRestApiException("Cannot parse label", e);
     }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
new file mode 100644
index 0000000..3230017
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
@@ -0,0 +1,172 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.LabelDefinitionJson;
+import com.google.gerrit.server.project.LabelResource;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class CreateLabel
+    implements RestCollectionCreateView<ProjectResource, LabelResource, LabelDefinitionInput> {
+  private final PermissionBackend permissionBackend;
+  private final MetaDataUpdate.User updateFactory;
+  private final ProjectConfig.Factory projectConfigFactory;
+  private final ProjectCache projectCache;
+
+  @Inject
+  public CreateLabel(
+      PermissionBackend permissionBackend,
+      MetaDataUpdate.User updateFactory,
+      ProjectConfig.Factory projectConfigFactory,
+      ProjectCache projectCache) {
+    this.permissionBackend = permissionBackend;
+    this.updateFactory = updateFactory;
+    this.projectConfigFactory = projectConfigFactory;
+    this.projectCache = projectCache;
+  }
+
+  @Override
+  public Response<LabelDefinitionInfo> apply(
+      ProjectResource rsrc, IdString id, LabelDefinitionInput input)
+      throws AuthException, BadRequestException, ResourceConflictException,
+          PermissionBackendException, IOException, ConfigInvalidException {
+    permissionBackend
+        .currentUser()
+        .project(rsrc.getNameKey())
+        .check(ProjectPermission.WRITE_CONFIG);
+
+    if (input == null) {
+      input = new LabelDefinitionInput();
+    }
+
+    if (input.name != null && !input.name.equals(id.get())) {
+      throw new BadRequestException("name in input must match name in URL");
+    }
+
+    try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) {
+      ProjectConfig config = projectConfigFactory.read(md);
+
+      if (config.getLabelSections().containsKey(id.get())) {
+        throw new ResourceConflictException("label " + id.get() + " already exists");
+      }
+
+      if (input.values == null || input.values.isEmpty()) {
+        throw new BadRequestException("values are required");
+      }
+
+      List<LabelValue> values = LabelDefinitionInputParser.parseValues(input.values);
+
+      LabelType labelType;
+      try {
+        labelType = new LabelType(id.get(), values);
+      } catch (IllegalArgumentException e) {
+        throw new BadRequestException("invalid name: " + id.get(), e);
+      }
+
+      if (input.function != null && !input.function.trim().isEmpty()) {
+        labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function));
+      } else {
+        labelType.setFunction(LabelFunction.MAX_WITH_BLOCK);
+      }
+
+      if (input.defaultValue != null) {
+        labelType.setDefaultValue(
+            LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue));
+      }
+
+      if (input.branches != null) {
+        labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches));
+      }
+
+      if (input.canOverride != null) {
+        labelType.setCanOverride(input.canOverride);
+      }
+
+      if (input.copyAnyScore != null) {
+        labelType.setCopyAnyScore(input.copyAnyScore);
+      }
+
+      if (input.copyMinScore != null) {
+        labelType.setCopyMinScore(input.copyMinScore);
+      }
+
+      if (input.copyMaxScore != null) {
+        labelType.setCopyMaxScore(input.copyMaxScore);
+      }
+
+      if (input.copyAllScoresIfNoChange != null) {
+        labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange);
+      }
+
+      if (input.copyAllScoresIfNoCodeChange != null) {
+        labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange);
+      }
+
+      if (input.copyAllScoresOnTrivialRebase != null) {
+        labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase);
+      }
+
+      if (input.copyAllScoresOnMergeFirstParentUpdate != null) {
+        labelType.setCopyAllScoresOnMergeFirstParentUpdate(
+            input.copyAllScoresOnMergeFirstParentUpdate);
+      }
+
+      if (input.allowPostSubmit != null) {
+        labelType.setAllowPostSubmit(input.allowPostSubmit);
+      }
+
+      if (input.ignoreSelfApproval != null) {
+        labelType.setIgnoreSelfApproval(input.ignoreSelfApproval);
+      }
+
+      if (input.commitMessage != null) {
+        md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
+      } else {
+        md.setMessage("Update label");
+      }
+
+      config.getLabelSections().put(labelType.getName(), labelType);
+      config.commit(md);
+
+      projectCache.evict(rsrc.getProjectState().getProject());
+
+      return Response.created(LabelDefinitionJson.format(rsrc.getNameKey(), labelType));
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
new file mode 100644
index 0000000..a45c67f
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.common.primitives.Shorts;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.InvalidNameException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.server.project.RefPattern;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+
+public class LabelDefinitionInputParser {
+  public static LabelFunction parseFunction(String functionString) throws BadRequestException {
+    Optional<LabelFunction> function = LabelFunction.parse(functionString.trim());
+    return function.orElseThrow(
+        () -> new BadRequestException("unknown function: " + functionString));
+  }
+
+  public static List<LabelValue> parseValues(Map<String, String> values)
+      throws BadRequestException {
+    List<LabelValue> valueList = new ArrayList<>();
+    for (Entry<String, String> e : values.entrySet()) {
+      short value;
+      try {
+        value = Shorts.checkedCast(PermissionRule.parseInt(e.getKey().trim()));
+      } catch (NumberFormatException ex) {
+        throw new BadRequestException("invalid value: " + e.getKey(), ex);
+      }
+      String valueDescription = e.getValue().trim();
+      if (valueDescription.isEmpty()) {
+        throw new BadRequestException("description for value '" + e.getKey() + "' cannot be empty");
+      }
+      valueList.add(new LabelValue(value, valueDescription));
+    }
+    return valueList;
+  }
+
+  public static short parseDefaultValue(LabelType labelType, short defaultValue)
+      throws BadRequestException {
+    if (labelType.getValue(defaultValue) == null) {
+      throw new BadRequestException("invalid default value: " + defaultValue);
+    }
+    return defaultValue;
+  }
+
+  public static List<String> parseBranches(List<String> branches) throws BadRequestException {
+    List<String> validBranches = new ArrayList<>();
+    for (String branch : branches) {
+      String newBranch = branch.trim();
+      if (newBranch.isEmpty()) {
+        continue;
+      }
+      if (!RefPattern.isRE(newBranch) && !newBranch.startsWith(RefNames.REFS)) {
+        newBranch = RefNames.REFS_HEADS + newBranch;
+      }
+      try {
+        RefPattern.validate(newBranch);
+      } catch (InvalidNameException e) {
+        throw new BadRequestException("invalid branch: " + branch, e);
+      }
+      validBranches.add(newBranch);
+    }
+    return validBranches;
+  }
+
+  private LabelDefinitionInputParser() {}
+}
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index d8ea436..7ec4e6d 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -68,6 +68,7 @@
     get(CHILD_PROJECT_KIND).to(GetChildProject.class);
 
     child(PROJECT_KIND, "labels").to(LabelsCollection.class);
+    create(LABEL_KIND).to(CreateLabel.class);
     get(LABEL_KIND).to(GetLabel.class);
     put(LABEL_KIND).to(SetLabel.class);
 
diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java
index b61a953..e5459fb 100644
--- a/java/com/google/gerrit/server/restapi/project/SetLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java
@@ -15,13 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import com.google.common.base.Strings;
-import com.google.common.primitives.Shorts;
-import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.common.LabelDefinitionInfo;
 import com.google.gerrit.extensions.common.LabelDefinitionInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -37,14 +31,9 @@
 import com.google.gerrit.server.project.LabelResource;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.RefPattern;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map.Entry;
-import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
@@ -106,15 +95,10 @@
       }
 
       if (input.function != null) {
-        String newFunctionName = input.function.trim();
-        if (newFunctionName.isEmpty()) {
+        if (input.function.trim().isEmpty()) {
           throw new BadRequestException("function cannot be empty");
         }
-        Optional<LabelFunction> newFunction = LabelFunction.parse(newFunctionName);
-        if (!newFunction.isPresent()) {
-          throw new BadRequestException("unknown function: " + input.function);
-        }
-        labelType.setFunction(newFunction.get());
+        labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function));
         dirty = true;
       }
 
@@ -122,52 +106,18 @@
         if (input.values.isEmpty()) {
           throw new BadRequestException("values cannot be empty");
         }
-
-        List<LabelValue> newValues = new ArrayList<>();
-        for (Entry<String, String> e : input.values.entrySet()) {
-          short value;
-          try {
-            value = Shorts.checkedCast(PermissionRule.parseInt(e.getKey().trim()));
-          } catch (NumberFormatException ex) {
-            throw new BadRequestException("invalid value: " + e.getKey(), ex);
-          }
-          String valueDescription = e.getValue().trim();
-          if (valueDescription.isEmpty()) {
-            throw new BadRequestException(
-                "description for value '" + e.getKey() + "' cannot be empty");
-          }
-          newValues.add(new LabelValue(value, valueDescription));
-        }
-        labelType.setValues(newValues);
+        labelType.setValues(LabelDefinitionInputParser.parseValues(input.values));
         dirty = true;
       }
 
       if (input.defaultValue != null) {
-        if (labelType.getValue(input.defaultValue) == null) {
-          throw new BadRequestException("invalid default value: " + input.defaultValue);
-        }
-        labelType.setDefaultValue(input.defaultValue);
+        labelType.setDefaultValue(
+            LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue));
         dirty = true;
       }
 
       if (input.branches != null) {
-        List<String> newBranches = new ArrayList<>();
-        for (String branch : input.branches) {
-          String newBranch = branch.trim();
-          if (newBranch.isEmpty()) {
-            continue;
-          }
-          if (!RefPattern.isRE(newBranch) && !newBranch.startsWith(RefNames.REFS)) {
-            newBranch = RefNames.REFS_HEADS + newBranch;
-          }
-          try {
-            RefPattern.validate(newBranch);
-          } catch (InvalidNameException e) {
-            throw new BadRequestException("invalid branch: " + branch, e);
-          }
-          newBranches.add(newBranch);
-        }
-        labelType.setRefPatterns(newBranches);
+        labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches));
         dirty = true;
       }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index 79c44d8..d39567b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -83,7 +83,8 @@
               // GET /projects/<project>/branches/<branch>/commits is not implemented
               .expectedResponseCode(SC_NOT_FOUND)
               .build(),
-          RestCall.get("/projects/%s/dashboards"));
+          RestCall.get("/projects/%s/dashboards"),
+          RestCall.put("/projects/%s/labels/new-label"));
 
   /**
    * Child project REST endpoints to be tested, each URL contains placeholders for the parent
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
new file mode 100644
index 0000000..2754ba8
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
@@ -0,0 +1,567 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+@NoHttpd
+public class CreateLabelIT extends AbstractDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ProjectOperations projectOperations;
+
+  @Test
+  public void notAllowed() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () ->
+                gApi.projects()
+                    .name(project.get())
+                    .label("Foo-Review")
+                    .create(new LabelDefinitionInput()));
+    assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted");
+  }
+
+  @Test
+  public void cannotCreateLabelIfNameDoesntMatch() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.name = "Foo";
+
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(project.get()).label("Bar").create(input));
+    assertThat(thrown).hasMessageThat().contains("name in input must match name in URL");
+  }
+
+  @Test
+  public void cannotCreateLabelWithNameThatIsAlreadyInUse() throws Exception {
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () ->
+                gApi.projects()
+                    .name(allProjects.get())
+                    .label("Code-Review")
+                    .create(new LabelDefinitionInput()));
+    assertThat(thrown).hasMessageThat().contains("label Code-Review already exists");
+  }
+
+  @Test
+  public void cannotCreateLabelWithInvalidName() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad");
+
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(project.get()).label("INVALID_NAME").create(input));
+    assertThat(thrown).hasMessageThat().contains("invalid name: INVALID_NAME");
+  }
+
+  @Test
+  public void cannotCreateLabelWithoutValues() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(project.get()).label("Foo").create(input));
+    assertThat(thrown).hasMessageThat().contains("values are required");
+
+    input.values = ImmutableMap.of();
+    thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(project.get()).label("Foo").create(input));
+    assertThat(thrown).hasMessageThat().contains("values are required");
+  }
+
+  @Test
+  public void cannotCreateLabelWithInvalidValues() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("invalidValue", "description");
+
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(project.get()).label("Foo").create(input));
+    assertThat(thrown).hasMessageThat().contains("invalid value: invalidValue");
+  }
+
+  @Test
+  public void cannotCreateLabelWithValuesThatHaveEmptyDescription() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "");
+
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(project.get()).label("Foo").create(input));
+    assertThat(thrown).hasMessageThat().contains("description for value '+1' cannot be empty");
+  }
+
+  @Test
+  public void cannotCreateLabelWithInvalidDefaultValue() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad");
+    input.defaultValue = 5;
+
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(project.get()).label("Foo").create(input));
+    assertThat(thrown).hasMessageThat().contains("invalid default value: " + input.defaultValue);
+  }
+
+  @Test
+  public void cannotCreateLabelWithUnknownFunction() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad");
+    input.function = "UnknownFuction";
+
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(project.get()).label("Foo").create(input));
+    assertThat(thrown).hasMessageThat().contains("unknown function: " + input.function);
+  }
+
+  @Test
+  public void cannotCreateLabelWithInvalidBranch() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad");
+    input.branches = ImmutableList.of("refs heads master");
+
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(project.get()).label("Foo").create(input));
+    assertThat(thrown).hasMessageThat().contains("invalid branch: refs heads master");
+  }
+
+  @Test
+  public void createWithNameAndValuesOnly() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("Foo").create(input).get();
+
+    assertThat(createdLabel.name).isEqualTo("Foo");
+    assertThat(createdLabel.projectName).isEqualTo(project.get());
+    assertThat(createdLabel.function).isEqualTo(LabelFunction.MAX_WITH_BLOCK.getFunctionName());
+    assertThat(createdLabel.values).containsExactlyEntriesIn(input.values);
+    assertThat(createdLabel.defaultValue).isEqualTo(0);
+    assertThat(createdLabel.branches).isNull();
+    assertThat(createdLabel.canOverride).isTrue();
+    assertThat(createdLabel.copyAnyScore).isNull();
+    assertThat(createdLabel.copyMinScore).isNull();
+    assertThat(createdLabel.copyMaxScore).isNull();
+    assertThat(createdLabel.copyAllScoresIfNoChange).isTrue();
+    assertThat(createdLabel.copyAllScoresIfNoCodeChange).isNull();
+    assertThat(createdLabel.copyAllScoresOnTrivialRebase).isNull();
+    assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isNull();
+    assertThat(createdLabel.allowPostSubmit).isTrue();
+    assertThat(createdLabel.ignoreSelfApproval).isNull();
+  }
+
+  @Test
+  public void createWithFunction() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.function = LabelFunction.NO_OP.getFunctionName();
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("Foo").create(input).get();
+
+    assertThat(createdLabel.function).isEqualTo(LabelFunction.NO_OP.getFunctionName());
+  }
+
+  @Test
+  public void functionEmptyAfterTrim() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.function = " ";
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("Foo").create(input).get();
+
+    assertThat(createdLabel.function).isEqualTo(LabelFunction.MAX_WITH_BLOCK.getFunctionName());
+  }
+
+  @Test
+  public void valuesAndDescriptionsAreTrimmed() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    // Positive values can be specified as '<value>' or '+<value>'.
+    input.values =
+        ImmutableMap.of(
+            " 2 ",
+            " Looks Very Good ",
+            " +1 ",
+            " Looks Good ",
+            " 0 ",
+            " Don't Know ",
+            " -1 ",
+            " Looks Bad ",
+            " -2 ",
+            " Looks Very Bad ");
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("Foo").create(input).get();
+    assertThat(createdLabel.values)
+        .containsExactly(
+            "+2", "Looks Very Good",
+            "+1", "Looks Good",
+            " 0", "Don't Know",
+            "-1", "Looks Bad",
+            "-2", "Looks Very Bad");
+  }
+
+  @Test
+  public void createWithDefaultValue() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.defaultValue = 1;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("Foo").create(input).get();
+
+    assertThat(createdLabel.defaultValue).isEqualTo(input.defaultValue);
+  }
+
+  @Test
+  public void createWithBranches() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    // Branches can be full ref, ref pattern or regular expression.
+    input.branches =
+        ImmutableList.of("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*");
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("Foo").create(input).get();
+    assertThat(createdLabel.branches).containsExactlyElementsIn(input.branches);
+  }
+
+  @Test
+  public void branchesAreTrimmed() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.branches =
+        ImmutableList.of(" refs/heads/master ", " refs/heads/foo/* ", " ^refs/heads/stable-.* ");
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("Foo").create(input).get();
+    assertThat(createdLabel.branches)
+        .containsExactly("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*");
+  }
+
+  @Test
+  public void emptyBranchesAreIgnored() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.branches = ImmutableList.of("refs/heads/master", "", " ");
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("Foo").create(input).get();
+    assertThat(createdLabel.branches).containsExactly("refs/heads/master");
+  }
+
+  @Test
+  public void branchesAreAutomaticallyPrefixedWithRefsHeads() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.branches = ImmutableList.of("master", "refs/meta/config");
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("Foo").create(input).get();
+    assertThat(createdLabel.branches).containsExactly("refs/heads/master", "refs/meta/config");
+  }
+
+  @Test
+  public void createWithCanOverride() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.canOverride = true;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.canOverride).isTrue();
+  }
+
+  @Test
+  public void createWithoutCanOverride() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.canOverride = false;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.canOverride).isNull();
+  }
+
+  @Test
+  public void createWithCopyAnyScore() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyAnyScore = true;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyAnyScore).isTrue();
+  }
+
+  @Test
+  public void createWithoutCopyAnyScore() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyAnyScore = false;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyAnyScore).isNull();
+  }
+
+  @Test
+  public void createWithCopyMinScore() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyMinScore = true;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyMinScore).isTrue();
+  }
+
+  @Test
+  public void createWithoutCopyMinScore() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyMinScore = false;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyMinScore).isNull();
+  }
+
+  @Test
+  public void createWithCopyMaxScore() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyMaxScore = true;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyMaxScore).isTrue();
+  }
+
+  @Test
+  public void createWithoutCopyMaxScore() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyMaxScore = false;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyMaxScore).isNull();
+  }
+
+  @Test
+  public void createWithCopyAllScoresIfNoChange() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyAllScoresIfNoChange = true;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyAllScoresIfNoChange).isTrue();
+  }
+
+  @Test
+  public void createWithoutCopyAllScoresIfNoChange() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyAllScoresIfNoChange = false;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyAllScoresIfNoChange).isNull();
+  }
+
+  @Test
+  public void createWithCopyAllScoresIfNoCodeChange() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyAllScoresIfNoCodeChange = true;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyAllScoresIfNoCodeChange).isTrue();
+  }
+
+  @Test
+  public void createWithoutCopyAllScoresIfNoCodeChange() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyAllScoresIfNoCodeChange = false;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyAllScoresIfNoCodeChange).isNull();
+  }
+
+  @Test
+  public void createWithCopyAllScoresOnTrivialRebase() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyAllScoresOnTrivialRebase = true;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyAllScoresOnTrivialRebase).isTrue();
+  }
+
+  @Test
+  public void createWithoutCopyAllScoresOnTrivialRebase() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyAllScoresOnTrivialRebase = false;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyAllScoresOnTrivialRebase).isNull();
+  }
+
+  @Test
+  public void createWithCopyAllScoresOnMergeFirstParentUpdate() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyAllScoresOnMergeFirstParentUpdate = true;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isTrue();
+  }
+
+  @Test
+  public void createWithoutCopyAllScoresOnMergeFirstParentUpdate() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.copyAllScoresOnMergeFirstParentUpdate = false;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isNull();
+  }
+
+  @Test
+  public void createWithAllowPostSubmit() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.allowPostSubmit = true;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.allowPostSubmit).isTrue();
+  }
+
+  @Test
+  public void createWithoutAllowPostSubmit() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.allowPostSubmit = false;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.allowPostSubmit).isNull();
+  }
+
+  @Test
+  public void createWithIgnoreSelfApproval() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.ignoreSelfApproval = true;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.ignoreSelfApproval).isTrue();
+  }
+
+  @Test
+  public void createWithoutIgnoreSelfApproval() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.ignoreSelfApproval = false;
+
+    LabelDefinitionInfo createdLabel =
+        gApi.projects().name(project.get()).label("foo").create(input).get();
+    assertThat(createdLabel.ignoreSelfApproval).isNull();
+  }
+
+  @Test
+  public void defaultCommitMessage() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    gApi.projects().name(project.get()).label("Foo").create(input);
+    assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage())
+        .isEqualTo("Update label");
+  }
+
+  @Test
+  public void withCommitMessage() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.commitMessage = "Add Foo Label";
+    gApi.projects().name(project.get()).label("Foo").create(input);
+    assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage())
+        .isEqualTo(input.commitMessage);
+  }
+
+  @Test
+  public void commitMessageIsTrimmed() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    input.commitMessage = " Add Foo Label ";
+    gApi.projects().name(project.get()).label("Foo").create(input);
+    assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage())
+        .isEqualTo("Add Foo Label");
+  }
+}