LCOV - code coverage report
Current view: top level - server/project - ProjectConfig.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 840 924 90.9 %
Date: 2022-11-19 15:00:39 Functions: 108 119 90.8 %

          Line data    Source code
       1             : // Copyright (C) 2010 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.project;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkState;
      18             : import static com.google.common.collect.ImmutableList.toImmutableList;
      19             : import static com.google.gerrit.entities.Permission.isPermission;
      20             : import static com.google.gerrit.entities.Project.DEFAULT_SUBMIT_TYPE;
      21             : import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isValidPluginPermission;
      22             : import static java.util.Objects.requireNonNull;
      23             : import static java.util.stream.Collectors.toList;
      24             : 
      25             : import com.google.common.annotations.VisibleForTesting;
      26             : import com.google.common.base.CharMatcher;
      27             : import com.google.common.base.Joiner;
      28             : import com.google.common.base.Splitter;
      29             : import com.google.common.base.Strings;
      30             : import com.google.common.collect.ImmutableList;
      31             : import com.google.common.collect.ImmutableSet;
      32             : import com.google.common.collect.Maps;
      33             : import com.google.common.collect.Sets;
      34             : import com.google.common.flogger.FluentLogger;
      35             : import com.google.common.primitives.Shorts;
      36             : import com.google.gerrit.common.Nullable;
      37             : import com.google.gerrit.common.UsedAt;
      38             : import com.google.gerrit.common.data.GlobalCapability;
      39             : import com.google.gerrit.entities.AccessSection;
      40             : import com.google.gerrit.entities.AccountGroup;
      41             : import com.google.gerrit.entities.AccountsSection;
      42             : import com.google.gerrit.entities.Address;
      43             : import com.google.gerrit.entities.BooleanProjectConfig;
      44             : import com.google.gerrit.entities.BranchOrderSection;
      45             : import com.google.gerrit.entities.CachedProjectConfig;
      46             : import com.google.gerrit.entities.ConfiguredMimeTypes;
      47             : import com.google.gerrit.entities.ContributorAgreement;
      48             : import com.google.gerrit.entities.GroupDescription;
      49             : import com.google.gerrit.entities.GroupReference;
      50             : import com.google.gerrit.entities.LabelFunction;
      51             : import com.google.gerrit.entities.LabelType;
      52             : import com.google.gerrit.entities.LabelValue;
      53             : import com.google.gerrit.entities.NotifyConfig;
      54             : import com.google.gerrit.entities.NotifyConfig.NotifyType;
      55             : import com.google.gerrit.entities.Permission;
      56             : import com.google.gerrit.entities.PermissionRule;
      57             : import com.google.gerrit.entities.PermissionRule.Action;
      58             : import com.google.gerrit.entities.Project;
      59             : import com.google.gerrit.entities.RefNames;
      60             : import com.google.gerrit.entities.StoredCommentLinkInfo;
      61             : import com.google.gerrit.entities.SubmitRequirement;
      62             : import com.google.gerrit.entities.SubmitRequirementExpression;
      63             : import com.google.gerrit.entities.SubscribeSection;
      64             : import com.google.gerrit.exceptions.InvalidNameException;
      65             : import com.google.gerrit.extensions.client.InheritableBoolean;
      66             : import com.google.gerrit.extensions.client.ProjectState;
      67             : import com.google.gerrit.server.account.GroupBackend;
      68             : import com.google.gerrit.server.config.AllProjectsConfigProvider;
      69             : import com.google.gerrit.server.config.AllProjectsName;
      70             : import com.google.gerrit.server.config.ConfigUtil;
      71             : import com.google.gerrit.server.config.PluginConfig;
      72             : import com.google.gerrit.server.git.ValidationError;
      73             : import com.google.gerrit.server.git.meta.MetaDataUpdate;
      74             : import com.google.gerrit.server.git.meta.VersionedMetaData;
      75             : import com.google.inject.Inject;
      76             : import com.google.inject.Singleton;
      77             : import java.io.IOException;
      78             : import java.util.ArrayList;
      79             : import java.util.Arrays;
      80             : import java.util.Collection;
      81             : import java.util.Collections;
      82             : import java.util.EnumSet;
      83             : import java.util.HashMap;
      84             : import java.util.HashSet;
      85             : import java.util.LinkedHashMap;
      86             : import java.util.List;
      87             : import java.util.Locale;
      88             : import java.util.Map;
      89             : import java.util.Objects;
      90             : import java.util.Optional;
      91             : import java.util.Set;
      92             : import java.util.function.Consumer;
      93             : import java.util.regex.Pattern;
      94             : import java.util.regex.PatternSyntaxException;
      95             : import org.eclipse.jgit.errors.ConfigInvalidException;
      96             : import org.eclipse.jgit.lib.CommitBuilder;
      97             : import org.eclipse.jgit.lib.Config;
      98             : import org.eclipse.jgit.lib.ObjectId;
      99             : import org.eclipse.jgit.lib.Repository;
     100             : import org.eclipse.jgit.lib.StoredConfig;
     101             : import org.eclipse.jgit.revwalk.RevWalk;
     102             : 
     103             : public class ProjectConfig extends VersionedMetaData implements ValidationError.Sink {
     104         151 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     105             : 
     106             :   public static final String COMMENTLINK = "commentlink";
     107             :   public static final String LABEL = "label";
     108             :   public static final String KEY_LABEL_DESCRIPTION = "description";
     109             :   public static final String KEY_FUNCTION = "function";
     110             :   public static final String KEY_DEFAULT_VALUE = "defaultValue";
     111             :   public static final String KEY_ALLOW_POST_SUBMIT = "allowPostSubmit";
     112             :   public static final String KEY_IGNORE_SELF_APPROVAL = "ignoreSelfApproval";
     113             :   public static final String KEY_COPY_CONDITION = "copyCondition";
     114             :   public static final String KEY_VALUE = "value";
     115             :   public static final String KEY_CAN_OVERRIDE = "canOverride";
     116             :   public static final String KEY_BRANCH = "branch";
     117             : 
     118             :   public static final String SUBMIT_REQUIREMENT = "submit-requirement";
     119             :   public static final String KEY_SR_DESCRIPTION = "description";
     120             :   public static final String KEY_SR_APPLICABILITY_EXPRESSION = "applicableIf";
     121             :   public static final String KEY_SR_SUBMITTABILITY_EXPRESSION = "submittableIf";
     122             :   public static final String KEY_SR_OVERRIDE_EXPRESSION = "overrideIf";
     123             :   public static final String KEY_SR_OVERRIDE_IN_CHILD_PROJECTS = "canOverrideInChildProjects";
     124         151 :   public static final ImmutableSet<String> SR_KEYS =
     125         151 :       ImmutableSet.of(
     126             :           KEY_SR_DESCRIPTION,
     127             :           KEY_SR_APPLICABILITY_EXPRESSION,
     128             :           KEY_SR_SUBMITTABILITY_EXPRESSION,
     129             :           KEY_SR_OVERRIDE_EXPRESSION,
     130             :           KEY_SR_OVERRIDE_IN_CHILD_PROJECTS);
     131             : 
     132             :   public static final String KEY_MATCH = "match";
     133             :   public static final String KEY_LINK = "link";
     134             :   public static final String KEY_PREFIX = "prefix";
     135             :   public static final String KEY_SUFFIX = "suffix";
     136             :   public static final String KEY_TEXT = "text";
     137             :   public static final String KEY_ENABLED = "enabled";
     138             : 
     139             :   public static final String PROJECT_CONFIG = "project.config";
     140             : 
     141             :   private static final String PROJECT = "project";
     142             :   private static final String KEY_DESCRIPTION = "description";
     143             : 
     144             :   public static final String ACCESS = "access";
     145             :   private static final String KEY_INHERIT_FROM = "inheritFrom";
     146             :   private static final String KEY_GROUP_PERMISSIONS = "exclusiveGroupPermissions";
     147             : 
     148             :   private static final String ACCOUNTS = "accounts";
     149             :   private static final String KEY_SAME_GROUP_VISIBILITY = "sameGroupVisibility";
     150             : 
     151             :   private static final String BRANCH_ORDER = "branchOrder";
     152             :   private static final String BRANCH = "branch";
     153             : 
     154             :   private static final String CONTRIBUTOR_AGREEMENT = "contributor-agreement";
     155             :   private static final String KEY_ACCEPTED = "accepted";
     156             :   private static final String KEY_AUTO_VERIFY = "autoVerify";
     157             :   private static final String KEY_AGREEMENT_URL = "agreementUrl";
     158             :   private static final String KEY_MATCH_PROJECTS = "matchProjects";
     159             :   private static final String KEY_EXCLUDE_PROJECTS = "excludeProjects";
     160             : 
     161             :   private static final String NOTIFY = "notify";
     162             :   private static final String KEY_EMAIL = "email";
     163             :   private static final String KEY_FILTER = "filter";
     164             :   private static final String KEY_TYPE = "type";
     165             :   private static final String KEY_HEADER = "header";
     166             : 
     167             :   private static final String CAPABILITY = "capability";
     168             : 
     169             :   private static final String RECEIVE = "receive";
     170             :   private static final String KEY_CHECK_RECEIVED_OBJECTS = "checkReceivedObjects";
     171             : 
     172             :   private static final String SUBMIT = "submit";
     173             :   private static final String KEY_ACTION = "action";
     174             :   private static final String KEY_STATE = "state";
     175             : 
     176             :   private static final String KEY_MAX_OBJECT_SIZE_LIMIT = "maxObjectSizeLimit";
     177             : 
     178             :   private static final String SUBSCRIBE_SECTION = "allowSuperproject";
     179             :   private static final String SUBSCRIBE_MATCH_REFS = "matching";
     180             :   private static final String SUBSCRIBE_MULTI_MATCH_REFS = "all";
     181             : 
     182             :   private static final String DASHBOARD = "dashboard";
     183             :   private static final String KEY_DEFAULT = "default";
     184             :   private static final String KEY_LOCAL_DEFAULT = "local-default";
     185             : 
     186             :   private static final String LEGACY_PERMISSION_PUSH_TAG = "pushTag";
     187             :   private static final String LEGACY_PERMISSION_PUSH_SIGNED_TAG = "pushSignedTag";
     188             : 
     189             :   private static final String PLUGIN = "plugin";
     190             : 
     191         151 :   private static final ProjectState DEFAULT_STATE_VALUE = ProjectState.ACTIVE;
     192             : 
     193             :   private static final String EXTENSION_PANELS = "extension-panels";
     194             :   private static final String KEY_PANEL = "panel";
     195             : 
     196         151 :   private static final Pattern EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN = Pattern.compile("[, \t]{1,}");
     197             : 
     198             :   // Don't use an assisted factory, since instances created by an assisted factory retain references
     199             :   // to their enclosing injector. Instances of ProjectConfig are cached for a long time in the
     200             :   // ProjectCache, so this would retain lots more memory.
     201             :   @Singleton
     202             :   public static class Factory {
     203             :     private final AllProjectsName allProjectsName;
     204             :     private final AllProjectsConfigProvider allProjectsConfigProvider;
     205             : 
     206             :     @Inject
     207         152 :     Factory(AllProjectsName allProjectsName, AllProjectsConfigProvider allProjectsConfigProvider) {
     208         152 :       this.allProjectsName = allProjectsName;
     209         152 :       this.allProjectsConfigProvider = allProjectsConfigProvider;
     210         152 :     }
     211             : 
     212             :     public ProjectConfig create(Project.NameKey projectName) {
     213         151 :       return new ProjectConfig(
     214             :           projectName,
     215         151 :           projectName.equals(allProjectsName)
     216         151 :               ? allProjectsConfigProvider.get(allProjectsName)
     217         151 :               : Optional.empty(),
     218             :           allProjectsName);
     219             :     }
     220             : 
     221             :     public ProjectConfig read(MetaDataUpdate update) throws IOException, ConfigInvalidException {
     222         151 :       ProjectConfig r = create(update.getProjectName());
     223         151 :       r.load(update);
     224         151 :       return r;
     225             :     }
     226             : 
     227             :     public ProjectConfig read(MetaDataUpdate update, ObjectId id)
     228             :         throws IOException, ConfigInvalidException {
     229           0 :       ProjectConfig r = create(update.getProjectName());
     230           0 :       r.load(update, id);
     231           0 :       return r;
     232             :     }
     233             : 
     234             :     @UsedAt(UsedAt.Project.COLLABNET)
     235             :     public ProjectConfig read(Repository repo, Project.NameKey name)
     236             :         throws IOException, ConfigInvalidException {
     237           0 :       ProjectConfig r = create(name);
     238           0 :       r.load(repo);
     239           0 :       return r;
     240             :     }
     241             :   }
     242             : 
     243             :   private final Optional<StoredConfig> baseConfig;
     244             :   private final AllProjectsName allProjectsName;
     245             : 
     246             :   private Project project;
     247             :   private AccountsSection accountsSection;
     248             :   private GroupList groupList;
     249             :   private Map<String, AccessSection> accessSections;
     250             :   private BranchOrderSection branchOrderSection;
     251             :   private Map<String, ContributorAgreement> contributorAgreements;
     252             :   private Map<String, NotifyConfig> notifySections;
     253             :   private Map<String, LabelType> labelSections;
     254             :   private Map<String, SubmitRequirement> submitRequirementSections;
     255             :   private ConfiguredMimeTypes mimeTypes;
     256             :   private Map<Project.NameKey, SubscribeSection> subscribeSections;
     257             :   private Map<String, StoredCommentLinkInfo> commentLinkSections;
     258             :   private List<ValidationError> validationErrors;
     259             :   private ObjectId rulesId;
     260             :   private long maxObjectSizeLimit;
     261             :   private Map<String, Config> pluginConfigs;
     262             :   private Map<String, Config> projectLevelConfigs;
     263             :   private boolean checkReceivedObjects;
     264             :   private Set<String> sectionsWithUnknownPermissions;
     265             :   private boolean hasLegacyPermissions;
     266             :   private Map<String, List<String>> extensionPanelSections;
     267             : 
     268             :   /** Returns an immutable, thread-safe representation of this object that can be cached. */
     269             :   public CachedProjectConfig getCacheable() {
     270             :     CachedProjectConfig.Builder builder =
     271         151 :         CachedProjectConfig.builder()
     272         151 :             .setProject(project)
     273         151 :             .setAccountsSection(accountsSection)
     274         151 :             .setBranchOrderSection(Optional.ofNullable(branchOrderSection))
     275         151 :             .setMimeTypes(mimeTypes)
     276         151 :             .setRulesId(Optional.ofNullable(rulesId))
     277         151 :             .setRevision(Optional.ofNullable(getRevision()))
     278         151 :             .setMaxObjectSizeLimit(maxObjectSizeLimit)
     279         151 :             .setCheckReceivedObjects(checkReceivedObjects)
     280         151 :             .setExtensionPanelSections(extensionPanelSections);
     281         151 :     groupList.byUUID().values().forEach(g -> builder.addGroup(g));
     282         151 :     contributorAgreements.values().forEach(c -> builder.addContributorAgreement(c));
     283         151 :     notifySections.values().forEach(n -> builder.addNotifySection(n));
     284         151 :     subscribeSections.values().forEach(s -> builder.addSubscribeSection(s));
     285         151 :     commentLinkSections.values().forEach(c -> builder.addCommentLinkSection(c));
     286         151 :     labelSections.values().forEach(l -> builder.addLabelSection(l));
     287         151 :     submitRequirementSections.values().forEach(sr -> builder.addSubmitRequirementSection(sr));
     288         151 :     pluginConfigs
     289         151 :         .entrySet()
     290         151 :         .forEach(c -> builder.addPluginConfig(c.getKey(), c.getValue().toText()));
     291         151 :     projectLevelConfigs
     292         151 :         .entrySet()
     293         151 :         .forEach(c -> builder.addProjectLevelConfig(c.getKey(), c.getValue().toText()));
     294             : 
     295         151 :     if (projectName.equals(allProjectsName)) {
     296             :       // Filter out permissions that aren't allowed to be set on All-Projects
     297         151 :       accessSections
     298         151 :           .values()
     299         151 :           .forEach(
     300             :               a -> {
     301         151 :                 List<Permission.Builder> copy = new ArrayList<>();
     302         151 :                 for (Permission p : a.getPermissions()) {
     303         151 :                   if (Permission.canBeOnAllProjects(a.getName(), p.getName())) {
     304         151 :                     copy.add(p.toBuilder());
     305             :                   }
     306         151 :                 }
     307         151 :                 AccessSection section =
     308         151 :                     AccessSection.builder(a.getName())
     309         151 :                         .modifyPermissions(permissions -> permissions.addAll(copy))
     310         151 :                         .build();
     311         151 :                 builder.addAccessSection(section);
     312         151 :               });
     313             :     } else {
     314         148 :       accessSections.values().forEach(a -> builder.addAccessSection(a));
     315             :     }
     316         151 :     return builder.build();
     317             :   }
     318             : 
     319             :   public static StoredCommentLinkInfo buildCommentLink(Config cfg, String name)
     320             :       throws IllegalArgumentException {
     321           2 :     String match = cfg.getString(COMMENTLINK, name, KEY_MATCH);
     322           2 :     if (match != null) {
     323             :       // Unfortunately this validation isn't entirely complete. Clients
     324             :       // can have exceptions trying to evaluate the pattern if they don't
     325             :       // support a token used, even if the server does support the token.
     326             :       //
     327             :       // At the minimum, we can trap problems related to unmatched groups.
     328           2 :       Pattern.compile(match);
     329             :     }
     330             : 
     331           2 :     String link = cfg.getString(COMMENTLINK, name, KEY_LINK);
     332           2 :     String linkPrefix = cfg.getString(COMMENTLINK, name, KEY_PREFIX);
     333           2 :     String linkSuffix = cfg.getString(COMMENTLINK, name, KEY_SUFFIX);
     334           2 :     String linkText = cfg.getString(COMMENTLINK, name, KEY_TEXT);
     335             : 
     336           2 :     String rawEnabled = cfg.getString(COMMENTLINK, name, KEY_ENABLED);
     337             :     Boolean enabled;
     338           2 :     if (rawEnabled != null) {
     339           2 :       enabled = cfg.getBoolean(COMMENTLINK, name, KEY_ENABLED, true);
     340             :     } else {
     341           2 :       enabled = null;
     342             :     }
     343             : 
     344           2 :     if (Strings.isNullOrEmpty(match) && Strings.isNullOrEmpty(link) && enabled != null) {
     345           1 :       if (enabled) {
     346           1 :         return StoredCommentLinkInfo.enabled(name);
     347             :       }
     348           1 :       return StoredCommentLinkInfo.disabled(name);
     349             :     }
     350           2 :     return StoredCommentLinkInfo.builder(name)
     351           2 :         .setMatch(match)
     352           2 :         .setLink(link)
     353           2 :         .setPrefix(linkPrefix)
     354           2 :         .setSuffix(linkSuffix)
     355           2 :         .setText(linkText)
     356           2 :         .setEnabled(enabled)
     357           2 :         .setOverrideOnly(false)
     358           2 :         .build();
     359             :   }
     360             : 
     361             :   public void addCommentLinkSection(StoredCommentLinkInfo commentLink) {
     362           2 :     commentLinkSections.put(commentLink.getName(), commentLink);
     363           2 :   }
     364             : 
     365             :   public void removeCommentLinkSection(String name) {
     366           1 :     requireNonNull(name);
     367           1 :     requireNonNull(commentLinkSections.remove(name));
     368           1 :   }
     369             : 
     370             :   private ProjectConfig(
     371             :       Project.NameKey projectName,
     372             :       Optional<StoredConfig> baseConfig,
     373         151 :       AllProjectsName allProjectsName) {
     374         151 :     this.projectName = projectName;
     375         151 :     this.baseConfig = baseConfig;
     376         151 :     this.allProjectsName = allProjectsName;
     377         151 :   }
     378             : 
     379             :   public void load(Repository repo) throws IOException, ConfigInvalidException {
     380           1 :     super.load(projectName, repo);
     381           1 :   }
     382             : 
     383             :   public void load(Repository repo, @Nullable ObjectId revision)
     384             :       throws IOException, ConfigInvalidException {
     385         151 :     super.load(projectName, repo, revision);
     386         151 :   }
     387             : 
     388             :   public void load(RevWalk rw, @Nullable ObjectId revision)
     389             :       throws IOException, ConfigInvalidException {
     390          14 :     super.load(projectName, rw, revision);
     391          14 :   }
     392             : 
     393             :   public Project.NameKey getName() {
     394           4 :     return projectName;
     395             :   }
     396             : 
     397             :   public Project getProject() {
     398          61 :     return project;
     399             :   }
     400             : 
     401             :   public void setProject(Project.Builder project) {
     402           0 :     this.project = project.build();
     403           0 :   }
     404             : 
     405             :   public void updateProject(Consumer<Project.Builder> update) {
     406         151 :     Project.Builder builder = project.toBuilder();
     407         151 :     update.accept(builder);
     408         151 :     project = builder.build();
     409         151 :   }
     410             : 
     411             :   public AccountsSection getAccountsSection() {
     412           1 :     return accountsSection;
     413             :   }
     414             : 
     415             :   public void setAccountsSection(AccountsSection accountsSection) {
     416           1 :     this.accountsSection = accountsSection;
     417           1 :   }
     418             : 
     419             :   /** Returns an access section, {@code name} typically is a ref pattern. */
     420             :   public AccessSection getAccessSection(String name) {
     421           2 :     return accessSections.get(name);
     422             :   }
     423             : 
     424             :   public void upsertAccessSection(String name, Consumer<AccessSection.Builder> update) {
     425             :     AccessSection.Builder accessSectionBuilder =
     426         151 :         accessSections.containsKey(name)
     427         151 :             ? accessSections.get(name).toBuilder()
     428         151 :             : AccessSection.builder(name);
     429         151 :     update.accept(accessSectionBuilder);
     430         151 :     accessSections.put(name, accessSectionBuilder.build());
     431         151 :   }
     432             : 
     433             :   public Collection<AccessSection> getAccessSections() {
     434          16 :     return sort(accessSections.values());
     435             :   }
     436             : 
     437             :   public BranchOrderSection getBranchOrderSection() {
     438           1 :     return branchOrderSection;
     439             :   }
     440             : 
     441             :   public void setBranchOrderSection(BranchOrderSection branchOrderSection) {
     442           2 :     this.branchOrderSection = branchOrderSection;
     443           2 :   }
     444             : 
     445             :   public Map<Project.NameKey, SubscribeSection> getSubscribeSections() {
     446           2 :     return subscribeSections;
     447             :   }
     448             : 
     449             :   public void addSubscribeSection(SubscribeSection s) {
     450           2 :     subscribeSections.put(s.project(), s);
     451           2 :   }
     452             : 
     453             :   public void remove(AccessSection section) {
     454           3 :     if (section != null) {
     455           3 :       String name = section.getName();
     456           3 :       if (sectionsWithUnknownPermissions.contains(name)) {
     457           3 :         AccessSection.Builder a = accessSections.get(name).toBuilder();
     458           3 :         a.modifyPermissions(List::clear);
     459           3 :         accessSections.put(name, a.build());
     460           3 :       } else {
     461           3 :         accessSections.remove(name);
     462             :       }
     463             :     }
     464           3 :   }
     465             : 
     466             :   public void remove(AccessSection section, Permission permission) {
     467           1 :     if (permission == null) {
     468           0 :       remove(section);
     469           1 :     } else if (section != null) {
     470           1 :       AccessSection a =
     471           1 :           accessSections.get(section.getName()).toBuilder().remove(permission.toBuilder()).build();
     472           1 :       accessSections.put(section.getName(), a);
     473           1 :       if (a.getPermissions().isEmpty()) {
     474           0 :         remove(a);
     475             :       }
     476             :     }
     477           1 :   }
     478             : 
     479             :   public void remove(AccessSection section, Permission permission, PermissionRule rule) {
     480           1 :     if (rule == null) {
     481           0 :       remove(section, permission);
     482           1 :     } else if (section != null && permission != null) {
     483           1 :       AccessSection a = accessSections.get(section.getName());
     484           1 :       if (a == null) {
     485           0 :         return;
     486             :       }
     487           1 :       Permission p = a.getPermission(permission.getName());
     488           1 :       if (p == null) {
     489           0 :         return;
     490             :       }
     491           1 :       AccessSection.Builder accessSectionBuilder = a.toBuilder();
     492           1 :       Permission.Builder permissionBuilder =
     493           1 :           accessSectionBuilder.upsertPermission(permission.getName());
     494           1 :       permissionBuilder.remove(rule);
     495           1 :       if (permissionBuilder.build().getRules().isEmpty()) {
     496           1 :         accessSectionBuilder.remove(permissionBuilder);
     497             :       }
     498           1 :       a = accessSectionBuilder.build();
     499           1 :       accessSections.put(section.getName(), a);
     500           1 :       if (a.getPermissions().isEmpty()) {
     501           1 :         remove(a);
     502             :       }
     503             :     }
     504           1 :   }
     505             : 
     506             :   public ContributorAgreement getContributorAgreement(String name) {
     507           1 :     return contributorAgreements.get(name);
     508             :   }
     509             : 
     510             :   public Collection<ContributorAgreement> getContributorAgreements() {
     511           0 :     return sort(contributorAgreements.values());
     512             :   }
     513             : 
     514             :   public void replace(ContributorAgreement section) {
     515           1 :     ContributorAgreement.Builder ca = section.toBuilder();
     516           1 :     ca.setAutoVerify(resolve(section.getAutoVerify()));
     517           1 :     ImmutableList.Builder<PermissionRule> newRules = ImmutableList.builder();
     518           1 :     for (PermissionRule rule : section.getAccepted()) {
     519           1 :       newRules.add(rule.toBuilder().setGroup(resolve(rule.getGroup())).build());
     520           1 :     }
     521           1 :     ca.setAccepted(newRules.build());
     522             : 
     523           1 :     contributorAgreements.put(section.getName(), ca.build());
     524           1 :   }
     525             : 
     526             :   public Collection<NotifyConfig> getNotifyConfigs() {
     527           1 :     return notifySections.values();
     528             :   }
     529             : 
     530             :   public void putNotifyConfig(String name, NotifyConfig nc) {
     531           1 :     notifySections.put(name, nc);
     532           1 :   }
     533             : 
     534             :   public Map<String, LabelType> getLabelSections() {
     535          10 :     return labelSections;
     536             :   }
     537             : 
     538             :   public Map<String, SubmitRequirement> getSubmitRequirementSections() {
     539           9 :     return submitRequirementSections;
     540             :   }
     541             : 
     542             :   /** Adds or replaces the given {@link SubmitRequirement} in this config. */
     543             :   public void upsertSubmitRequirement(SubmitRequirement requirement) {
     544           5 :     submitRequirementSections.put(requirement.name(), requirement);
     545           5 :   }
     546             : 
     547             :   @VisibleForTesting
     548             :   public void clearSubmitRequirements() {
     549           1 :     submitRequirementSections = new LinkedHashMap<>();
     550           1 :   }
     551             : 
     552             :   /** Adds or replaces the given {@link LabelType} in this config. */
     553             :   public void upsertLabelType(LabelType labelType) {
     554         151 :     labelSections.put(labelType.getName(), labelType);
     555         151 :   }
     556             : 
     557             :   /** Allows a mutation of an existing {@link LabelType}. */
     558             :   public void updateLabelType(String name, Consumer<LabelType.Builder> update) {
     559           9 :     LabelType labelType = labelSections.get(name);
     560           9 :     checkState(labelType != null, "labelType must not be null");
     561           9 :     LabelType.Builder builder = labelSections.get(name).toBuilder();
     562           9 :     update.accept(builder);
     563           9 :     upsertLabelType(builder.build());
     564           9 :   }
     565             : 
     566             :   /** Adds or replaces the given {@link ContributorAgreement} in this config. */
     567             :   public void upsertContributorAgreement(ContributorAgreement ca) {
     568           1 :     contributorAgreements.remove(ca.getName());
     569           1 :     contributorAgreements.put(ca.getName(), ca);
     570           1 :   }
     571             : 
     572             :   public Collection<StoredCommentLinkInfo> getCommentLinkSections() {
     573           1 :     return commentLinkSections.values();
     574             :   }
     575             : 
     576             :   public ConfiguredMimeTypes getMimeTypes() {
     577           0 :     return mimeTypes;
     578             :   }
     579             : 
     580             :   public GroupReference resolve(GroupReference group) {
     581         151 :     return groupList.resolve(group);
     582             :   }
     583             : 
     584             :   public void renameGroup(AccountGroup.UUID uuid, String newName) {
     585           2 :     groupList.renameGroup(uuid, newName);
     586           2 :   }
     587             : 
     588             :   /** Returns the group reference, if the group is used by at least one rule. */
     589             :   public GroupReference getGroup(AccountGroup.UUID uuid) {
     590           1 :     return groupList.byUUID(uuid);
     591             :   }
     592             : 
     593             :   /**
     594             :    * Returns the group reference corresponding to the specified group name if the group is used by
     595             :    * at least one rule or plugin value.
     596             :    */
     597             :   public GroupReference getGroup(String groupName) {
     598           0 :     return groupList.byName(groupName);
     599             :   }
     600             : 
     601             :   /**
     602             :    * Returns the project's rules.pl ObjectId, if present in the branch. Null if it doesn't exist.
     603             :    */
     604             :   public ObjectId getRulesId() {
     605           0 :     return rulesId;
     606             :   }
     607             : 
     608             :   /** Returns the maxObjectSizeLimit configured on this project, or zero if not configured. */
     609             :   public long getMaxObjectSizeLimit() {
     610           0 :     return maxObjectSizeLimit;
     611             :   }
     612             : 
     613             :   /** Returns the checkReceivedObjects for this project, default is true. */
     614             :   public boolean getCheckReceivedObjects() {
     615           0 :     return checkReceivedObjects;
     616             :   }
     617             : 
     618             :   /**
     619             :    * Check all GroupReferences use current group name, repairing stale ones.
     620             :    *
     621             :    * @param groupBackend cache to use when looking up group information by UUID.
     622             :    * @return true if one or more group names was stale.
     623             :    */
     624             :   public boolean updateGroupNames(GroupBackend groupBackend) {
     625           9 :     boolean dirty = false;
     626           9 :     for (GroupReference ref : groupList.references()) {
     627           7 :       GroupDescription.Basic g = groupBackend.get(ref.getUUID());
     628           7 :       if (g != null && !g.getName().equals(ref.getName())) {
     629           0 :         dirty = true;
     630           0 :         groupList.renameGroup(ref.getUUID(), g.getName());
     631             :       }
     632           7 :     }
     633           9 :     return dirty;
     634             :   }
     635             : 
     636             :   /**
     637             :    * Get the validation errors, if any were discovered during load.
     638             :    *
     639             :    * @return list of errors; empty list if there are no errors.
     640             :    */
     641             :   public List<ValidationError> getValidationErrors() {
     642          15 :     if (validationErrors != null) {
     643           4 :       return Collections.unmodifiableList(validationErrors);
     644             :     }
     645          14 :     return Collections.emptyList();
     646             :   }
     647             : 
     648             :   @Override
     649             :   protected String getRefName() {
     650         151 :     return RefNames.REFS_CONFIG;
     651             :   }
     652             : 
     653             :   @Override
     654             :   protected void onLoad() throws IOException, ConfigInvalidException {
     655         151 :     if (baseConfig.isPresent()) {
     656         151 :       baseConfig.get().load();
     657             :     }
     658         151 :     readGroupList();
     659             : 
     660         151 :     rulesId = getObjectId("rules.pl");
     661         151 :     Config rc = readConfig(PROJECT_CONFIG, baseConfig);
     662         151 :     Project.Builder p = Project.builder(projectName);
     663         151 :     p.setDescription(Strings.nullToEmpty(rc.getString(PROJECT, null, KEY_DESCRIPTION)));
     664         151 :     if (revision != null) {
     665         151 :       p.setConfigRefState(revision.toObjectId().name());
     666             :     }
     667             : 
     668         151 :     if (rc.getStringList(ACCESS, null, KEY_INHERIT_FROM).length > 1) {
     669             :       // The config must not contain more than one parent to inherit from
     670             :       // as there is no guarantee which of the parents would be used then.
     671           1 :       error("Cannot inherit from multiple projects");
     672             :     }
     673         151 :     p.setParent(rc.getString(ACCESS, null, KEY_INHERIT_FROM));
     674             : 
     675         151 :     for (BooleanProjectConfig config : BooleanProjectConfig.values()) {
     676         151 :       p.setBooleanConfig(
     677             :           config,
     678         151 :           getEnum(
     679             :               rc,
     680         151 :               config.getSection(),
     681         151 :               config.getSubSection(),
     682         151 :               config.getName(),
     683             :               InheritableBoolean.INHERIT));
     684             :     }
     685             : 
     686         151 :     p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
     687             : 
     688         151 :     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, DEFAULT_SUBMIT_TYPE));
     689         151 :     p.setState(getEnum(rc, PROJECT, null, KEY_STATE, DEFAULT_STATE_VALUE));
     690             : 
     691         151 :     p.setDefaultDashboard(rc.getString(DASHBOARD, null, KEY_DEFAULT));
     692         151 :     p.setLocalDefaultDashboard(rc.getString(DASHBOARD, null, KEY_LOCAL_DEFAULT));
     693         151 :     this.project = p.build();
     694             : 
     695         151 :     loadAccountsSection(rc);
     696         151 :     loadContributorAgreements(rc);
     697         151 :     loadAccessSections(rc);
     698         151 :     loadBranchOrderSection(rc);
     699         151 :     loadNotifySections(rc);
     700         151 :     loadLabelSections(rc);
     701         151 :     loadSubmitRequirementSections(rc);
     702         151 :     loadCommentLinkSections(rc);
     703         151 :     loadSubscribeSections(rc);
     704         151 :     mimeTypes = ConfiguredMimeTypes.create(projectName.get(), rc);
     705         151 :     loadPluginSections(rc);
     706         151 :     loadProjectLevelConfigs();
     707         151 :     loadReceiveSection(rc);
     708         151 :     loadExtensionPanelSections(rc);
     709         151 :   }
     710             : 
     711             :   private void loadAccountsSection(Config rc) {
     712         151 :     accountsSection =
     713         151 :         AccountsSection.create(
     714         151 :             loadPermissionRules(rc, ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY, false));
     715         151 :   }
     716             : 
     717             :   private void loadExtensionPanelSections(Config rc) {
     718         151 :     Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
     719         151 :     extensionPanelSections = new LinkedHashMap<>();
     720         151 :     for (String name : rc.getSubsections(EXTENSION_PANELS)) {
     721           0 :       String lower = name.toLowerCase();
     722           0 :       if (lowerNames.containsKey(lower)) {
     723           0 :         error(
     724           0 :             String.format(
     725           0 :                 "Extension Panels \"%s\" conflicts with \"%s\"", name, lowerNames.get(lower)));
     726             :       }
     727           0 :       lowerNames.put(lower, name);
     728           0 :       extensionPanelSections.put(
     729             :           name,
     730           0 :           new ArrayList<>(Arrays.asList(rc.getStringList(EXTENSION_PANELS, name, KEY_PANEL))));
     731           0 :     }
     732         151 :   }
     733             : 
     734             :   private void loadContributorAgreements(Config rc) {
     735         151 :     contributorAgreements = new HashMap<>();
     736         151 :     for (String name : rc.getSubsections(CONTRIBUTOR_AGREEMENT)) {
     737           2 :       ContributorAgreement.Builder ca = ContributorAgreement.builder(name);
     738           2 :       ca.setDescription(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_DESCRIPTION));
     739           2 :       ca.setAgreementUrl(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_AGREEMENT_URL));
     740           2 :       ca.setAccepted(loadPermissionRules(rc, CONTRIBUTOR_AGREEMENT, name, KEY_ACCEPTED, false));
     741           2 :       ca.setExcludeProjectsRegexes(
     742           2 :           loadPatterns(rc, CONTRIBUTOR_AGREEMENT, name, KEY_EXCLUDE_PROJECTS));
     743           2 :       ca.setMatchProjectsRegexes(loadPatterns(rc, CONTRIBUTOR_AGREEMENT, name, KEY_MATCH_PROJECTS));
     744             : 
     745           2 :       List<PermissionRule> rules =
     746           2 :           loadPermissionRules(rc, CONTRIBUTOR_AGREEMENT, name, KEY_AUTO_VERIFY, false);
     747           2 :       if (rules.isEmpty()) {
     748           2 :         ca.setAutoVerify(null);
     749           2 :       } else if (rules.size() > 1) {
     750           0 :         error(
     751           0 :             String.format(
     752             :                 "Invalid rule in %s.%s.%s: at most one group may be set",
     753             :                 CONTRIBUTOR_AGREEMENT, name, KEY_AUTO_VERIFY));
     754           2 :       } else if (rules.get(0).getAction() != Action.ALLOW) {
     755           0 :         error(
     756           0 :             String.format(
     757             :                 "Invalid rule in %s.%s.%s: the group must be allowed",
     758             :                 CONTRIBUTOR_AGREEMENT, name, KEY_AUTO_VERIFY));
     759             :       } else {
     760           2 :         ca.setAutoVerify(rules.get(0).getGroup());
     761             :       }
     762           2 :       contributorAgreements.put(name, ca.build());
     763           2 :     }
     764         151 :   }
     765             : 
     766             :   /**
     767             :    * Parses the [notify] sections out of the configuration file.
     768             :    *
     769             :    * <pre>
     770             :    *   [notify "reviewers"]
     771             :    *     email = group Reviewers
     772             :    *     type = new_changes
     773             :    *
     774             :    *   [notify "dev-team"]
     775             :    *     email = dev-team@example.com
     776             :    *     filter = branch:master
     777             :    *
     778             :    *   [notify "qa"]
     779             :    *     email = qa@example.com
     780             :    *     filter = branch:\"^(maint|stable)-.*\"
     781             :    *     type = submitted_changes
     782             :    * </pre>
     783             :    */
     784             :   private void loadNotifySections(Config rc) {
     785         151 :     notifySections = new HashMap<>();
     786         151 :     for (String sectionName : rc.getSubsections(NOTIFY)) {
     787           3 :       NotifyConfig.Builder n = NotifyConfig.builder();
     788           3 :       n.setName(sectionName);
     789           3 :       n.setFilter(rc.getString(NOTIFY, sectionName, KEY_FILTER));
     790             : 
     791           3 :       EnumSet<NotifyType> types = EnumSet.noneOf(NotifyType.class);
     792           3 :       types.addAll(ConfigUtil.getEnumList(rc, NOTIFY, sectionName, KEY_TYPE, NotifyType.ALL));
     793           3 :       n.setNotify(types);
     794           3 :       n.setHeader(rc.getEnum(NOTIFY, sectionName, KEY_HEADER, NotifyConfig.Header.BCC));
     795             : 
     796           3 :       for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) {
     797           3 :         String groupName = GroupReference.extractGroupName(dst);
     798           3 :         if (groupName != null) {
     799           0 :           GroupReference ref = groupList.byName(groupName);
     800           0 :           if (ref == null) {
     801           0 :             ref = groupList.resolve(GroupReference.create(groupName));
     802             :           }
     803           0 :           if (ref.getUUID() != null) {
     804           0 :             n.addGroup(ref);
     805             :           } else {
     806           0 :             error(String.format("group \"%s\" not in %s", ref.getName(), GroupList.FILE_NAME));
     807             :           }
     808           3 :         } else if (dst.startsWith("user ")) {
     809           0 :           error(String.format("%s not supported", dst));
     810             :         } else {
     811             :           try {
     812           3 :             n.addAddress(Address.parse(dst));
     813           0 :           } catch (IllegalArgumentException err) {
     814           0 :             error(
     815           0 :                 String.format("notify section \"%s\" has invalid email \"%s\"", sectionName, dst));
     816           3 :           }
     817             :         }
     818             :       }
     819           3 :       notifySections.put(sectionName, n.build());
     820           3 :     }
     821         151 :   }
     822             : 
     823             :   private void loadAccessSections(Config rc) {
     824         151 :     accessSections = new HashMap<>();
     825         151 :     sectionsWithUnknownPermissions = new HashSet<>();
     826         151 :     for (String refName : rc.getSubsections(ACCESS)) {
     827         151 :       if (AccessSection.isValidRefSectionName(refName) && isValidRegex(refName)) {
     828         151 :         upsertAccessSection(
     829             :             refName,
     830             :             as -> {
     831         151 :               for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
     832         151 :                 for (String n : Splitter.on(EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN).split(varName)) {
     833         151 :                   n = convertLegacyPermission(n);
     834         151 :                   if (isCoreOrPluginPermission(n)) {
     835         151 :                     as.upsertPermission(n).setExclusiveGroup(true);
     836             :                   }
     837         151 :                 }
     838             :               }
     839             : 
     840         151 :               for (String varName : rc.getNames(ACCESS, refName)) {
     841         151 :                 String convertedName = convertLegacyPermission(varName);
     842         151 :                 if (isCoreOrPluginPermission(convertedName)) {
     843         151 :                   Permission.Builder perm = as.upsertPermission(convertedName);
     844         151 :                   loadPermissionRules(
     845         151 :                       rc, ACCESS, refName, varName, perm, Permission.hasRange(convertedName));
     846         151 :                 } else {
     847         151 :                   sectionsWithUnknownPermissions.add(as.getName());
     848             :                 }
     849         151 :               }
     850         151 :             });
     851             :       }
     852         151 :     }
     853             : 
     854         151 :     AccessSection.Builder capability = null;
     855         151 :     for (String varName : rc.getNames(CAPABILITY)) {
     856         151 :       if (capability == null) {
     857         151 :         capability = AccessSection.builder(AccessSection.GLOBAL_CAPABILITIES);
     858         151 :         accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability.build());
     859             :       }
     860         151 :       Permission.Builder perm = capability.upsertPermission(varName);
     861         151 :       loadPermissionRules(rc, CAPABILITY, null, varName, perm, GlobalCapability.hasRange(varName));
     862         151 :       accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability.build());
     863         151 :     }
     864         151 :   }
     865             : 
     866             :   private boolean isCoreOrPluginPermission(String permission) {
     867             :     // Since plugins are loaded dynamically, here we can't load all plugin permissions and verify
     868             :     // their existence.
     869         151 :     return isPermission(permission) || isValidPluginPermission(permission);
     870             :   }
     871             : 
     872             :   private boolean isValidRegex(String refPattern) {
     873             :     try {
     874         151 :       RefPattern.validateRegExp(refPattern);
     875           0 :     } catch (InvalidNameException e) {
     876           0 :       error(String.format("Invalid ref name: %s", e.getMessage()));
     877           0 :       return false;
     878         151 :     }
     879         151 :     return true;
     880             :   }
     881             : 
     882             :   private void loadBranchOrderSection(Config rc) {
     883         151 :     if (rc.getSections().contains(BRANCH_ORDER)) {
     884           2 :       branchOrderSection =
     885           2 :           BranchOrderSection.create(Arrays.asList(rc.getStringList(BRANCH_ORDER, null, BRANCH)));
     886             :     }
     887         151 :   }
     888             : 
     889             :   private void saveBranchOrderSection(Config rc) {
     890         151 :     if (branchOrderSection != null) {
     891           2 :       rc.setStringList(BRANCH_ORDER, null, BRANCH, branchOrderSection.order());
     892             :     }
     893         151 :   }
     894             : 
     895             :   private ImmutableList<String> loadPatterns(
     896             :       Config rc, String section, String subsection, String varName) {
     897           2 :     ImmutableList.Builder<String> patterns = ImmutableList.builder();
     898           2 :     for (String patternString : rc.getStringList(section, subsection, varName)) {
     899             :       try {
     900             :         // While one could just use getStringList directly, compiling first will cause the server
     901             :         // to fail fast if any of the patterns are invalid.
     902           2 :         patterns.add(Pattern.compile(patternString).pattern());
     903           0 :       } catch (PatternSyntaxException e) {
     904           0 :         error(String.format("Invalid regular expression: %s", e.getMessage()));
     905           0 :         continue;
     906           2 :       }
     907             :     }
     908           2 :     return patterns.build();
     909             :   }
     910             : 
     911             :   private ImmutableList<PermissionRule> loadPermissionRules(
     912             :       Config rc, String section, String subsection, String varName, boolean useRange) {
     913         151 :     Permission.Builder perm = Permission.builder(varName);
     914         151 :     loadPermissionRules(rc, section, subsection, varName, perm, useRange);
     915         151 :     return perm.build().getRules();
     916             :   }
     917             : 
     918             :   private void loadPermissionRules(
     919             :       Config rc,
     920             :       String section,
     921             :       String subsection,
     922             :       String varName,
     923             :       Permission.Builder perm,
     924             :       boolean useRange) {
     925         151 :     for (String ruleString : rc.getStringList(section, subsection, varName)) {
     926             :       PermissionRule rule;
     927             :       try {
     928         151 :         rule = PermissionRule.fromString(ruleString, useRange);
     929           0 :       } catch (IllegalArgumentException notRule) {
     930           0 :         error(
     931           0 :             String.format(
     932             :                 "Invalid rule in %s.%s: %s",
     933           0 :                 section + (subsection != null ? "." + subsection : ""),
     934             :                 varName,
     935           0 :                 notRule.getMessage()));
     936           0 :         continue;
     937         151 :       }
     938             : 
     939         151 :       GroupReference ref = groupList.byName(rule.getGroup().getName());
     940         151 :       if (ref == null) {
     941             :         // The group wasn't mentioned in the groups table, so there is
     942             :         // no valid UUID for it. Pool the reference anyway so at least
     943             :         // all rules in the same file share the same GroupReference.
     944             :         //
     945           1 :         ref = groupList.resolve(rule.getGroup());
     946           1 :         error(String.format("group \"%s\" not in %s", ref.getName(), GroupList.FILE_NAME));
     947             :       }
     948             : 
     949         151 :       perm.add(rule.toBuilder().setGroup(ref));
     950             :     }
     951         151 :   }
     952             : 
     953             :   private static LabelValue parseLabelValue(String src) {
     954             :     List<String> parts =
     955         151 :         ImmutableList.copyOf(
     956         151 :             Splitter.on(CharMatcher.whitespace()).omitEmptyStrings().limit(2).split(src));
     957         151 :     if (parts.isEmpty()) {
     958           0 :       throw new IllegalArgumentException("empty value");
     959             :     }
     960         151 :     String valueText = parts.size() > 1 ? parts.get(1) : "";
     961         151 :     return LabelValue.create(Shorts.checkedCast(PermissionRule.parseInt(parts.get(0))), valueText);
     962             :   }
     963             : 
     964             :   private void loadSubmitRequirementSections(Config rc) {
     965         151 :     checkForUnsupportedSubmitRequirementParams(rc);
     966             : 
     967         151 :     Map<String, String> lowerNames = new HashMap<>();
     968         151 :     submitRequirementSections = new LinkedHashMap<>();
     969         151 :     for (String name : rc.getSubsections(SUBMIT_REQUIREMENT)) {
     970           6 :       checkDuplicateSrDefinition(rc, name);
     971           6 :       String lower = name.toLowerCase();
     972           6 :       if (lowerNames.containsKey(lower)) {
     973           3 :         error(
     974           3 :             String.format(
     975           3 :                 "Submit requirement '%s' conflicts with '%s'.", name, lowerNames.get(lower)));
     976           3 :         continue;
     977             :       }
     978           6 :       lowerNames.put(lower, name);
     979           6 :       String description = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_DESCRIPTION);
     980           6 :       String applicabilityExpr =
     981           6 :           rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_APPLICABILITY_EXPRESSION);
     982           6 :       String submittabilityExpr =
     983           6 :           rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_SUBMITTABILITY_EXPRESSION);
     984           6 :       String overrideExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_EXPRESSION);
     985             :       boolean canInherit;
     986             :       try {
     987           6 :         canInherit =
     988           6 :             rc.getBoolean(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, false);
     989           1 :       } catch (IllegalArgumentException e) {
     990           1 :         String canInheritValue =
     991           1 :             rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS);
     992           1 :         error(
     993           1 :             String.format(
     994             :                 "Invalid value %s.%s.%s for submit requirement '%s': %s",
     995             :                 SUBMIT_REQUIREMENT,
     996             :                 name,
     997             :                 KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
     998             :                 name,
     999             :                 canInheritValue));
    1000           1 :         continue;
    1001           6 :       }
    1002             : 
    1003           6 :       if (submittabilityExpr == null) {
    1004           3 :         error(
    1005           3 :             String.format(
    1006             :                 "Setting a submittability expression for submit requirement '%s' is required:"
    1007             :                     + " Missing %s.%s.%s",
    1008             :                 name, SUBMIT_REQUIREMENT, name, KEY_SR_SUBMITTABILITY_EXPRESSION));
    1009           3 :         continue;
    1010             :       }
    1011             : 
    1012             :       // The expressions are validated in SubmitRequirementConfigValidator.
    1013             : 
    1014             :       SubmitRequirement submitRequirement =
    1015           6 :           SubmitRequirement.builder()
    1016           6 :               .setName(name)
    1017           6 :               .setDescription(Optional.ofNullable(description))
    1018           6 :               .setApplicabilityExpression(SubmitRequirementExpression.of(applicabilityExpr))
    1019           6 :               .setSubmittabilityExpression(SubmitRequirementExpression.create(submittabilityExpr))
    1020           6 :               .setOverrideExpression(SubmitRequirementExpression.of(overrideExpr))
    1021           6 :               .setAllowOverrideInChildProjects(canInherit)
    1022           6 :               .build();
    1023             : 
    1024           6 :       submitRequirementSections.put(name, submitRequirement);
    1025           6 :     }
    1026         151 :   }
    1027             : 
    1028             :   private void checkDuplicateSrDefinition(Config rc, String srName) {
    1029           6 :     if (rc.getStringList(SUBMIT_REQUIREMENT, srName, KEY_SR_DESCRIPTION).length > 1) {
    1030           1 :       error(
    1031           1 :           String.format(
    1032             :               "Multiple definitions of %s for submit requirement '%s'",
    1033             :               KEY_SR_DESCRIPTION, srName));
    1034             :     }
    1035           6 :     if (rc.getStringList(SUBMIT_REQUIREMENT, srName, KEY_SR_APPLICABILITY_EXPRESSION).length > 1) {
    1036           1 :       error(
    1037           1 :           String.format(
    1038             :               "Multiple definitions of %s for submit requirement '%s'",
    1039             :               KEY_SR_APPLICABILITY_EXPRESSION, srName));
    1040             :     }
    1041           6 :     if (rc.getStringList(SUBMIT_REQUIREMENT, srName, KEY_SR_SUBMITTABILITY_EXPRESSION).length > 1) {
    1042           2 :       error(
    1043           2 :           String.format(
    1044             :               "Multiple definitions of %s for submit requirement '%s'",
    1045             :               KEY_SR_SUBMITTABILITY_EXPRESSION, srName));
    1046             :     }
    1047           6 :     if (rc.getStringList(SUBMIT_REQUIREMENT, srName, KEY_SR_OVERRIDE_EXPRESSION).length > 1) {
    1048           1 :       error(
    1049           1 :           String.format(
    1050             :               "Multiple definitions of %s for submit requirement '%s'",
    1051             :               KEY_SR_OVERRIDE_EXPRESSION, srName));
    1052             :     }
    1053           6 :     if (rc.getStringList(SUBMIT_REQUIREMENT, srName, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS).length
    1054             :         > 1) {
    1055           1 :       error(
    1056           1 :           String.format(
    1057             :               "Multiple definitions of %s for submit requirement '%s'",
    1058             :               KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, srName));
    1059             :     }
    1060           6 :   }
    1061             : 
    1062             :   /**
    1063             :    * Report unsupported submit requirement parameters as errors.
    1064             :    *
    1065             :    * <p>Unsupported are submit requirements parameters that
    1066             :    *
    1067             :    * <ul>
    1068             :    *   <li>are directly set in the {@code submit-requirement} section (as submit requirements are
    1069             :    *       solely defined in subsections)
    1070             :    *   <li>are unknown (maybe they were accidentally misspelled?)
    1071             :    * </ul>
    1072             :    */
    1073             :   private void checkForUnsupportedSubmitRequirementParams(Config rc) {
    1074         151 :     Set<String> directSubmitRequirementParams = rc.getNames(SUBMIT_REQUIREMENT);
    1075         151 :     if (!directSubmitRequirementParams.isEmpty()) {
    1076           1 :       error(
    1077           1 :           String.format(
    1078             :               "Submit requirements must be defined in %s.<name> subsections."
    1079             :                   + " Setting parameters directly in the %s section is not allowed: %s",
    1080             :               SUBMIT_REQUIREMENT,
    1081             :               SUBMIT_REQUIREMENT,
    1082           1 :               directSubmitRequirementParams.stream().sorted().collect(toImmutableList())));
    1083             :     }
    1084             : 
    1085         151 :     for (String subsection : rc.getSubsections(SUBMIT_REQUIREMENT)) {
    1086           6 :       ImmutableList<String> unknownSubmitRequirementParams =
    1087           6 :           rc.getNames(SUBMIT_REQUIREMENT, subsection).stream()
    1088           6 :               .filter(p -> !SR_KEYS.contains(p))
    1089           6 :               .collect(toImmutableList());
    1090           6 :       if (!unknownSubmitRequirementParams.isEmpty()) {
    1091           1 :         error(
    1092           1 :             String.format(
    1093             :                 "Unsupported parameters for submit requirement '%s': %s",
    1094             :                 subsection, unknownSubmitRequirementParams));
    1095             :       }
    1096           6 :     }
    1097         151 :   }
    1098             : 
    1099             :   private void loadLabelSections(Config rc) {
    1100         151 :     Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
    1101         151 :     labelSections = new LinkedHashMap<>();
    1102         151 :     for (String name : rc.getSubsections(LABEL)) {
    1103         151 :       String lower = name.toLowerCase();
    1104         151 :       if (lowerNames.containsKey(lower)) {
    1105           0 :         error(String.format("Label \"%s\" conflicts with \"%s\"", name, lowerNames.get(lower)));
    1106             :       }
    1107         151 :       lowerNames.put(lower, name);
    1108             : 
    1109         151 :       List<LabelValue> values = new ArrayList<>();
    1110         151 :       Set<Short> allValues = new HashSet<>();
    1111         151 :       for (String value : rc.getStringList(LABEL, name, KEY_VALUE)) {
    1112             :         try {
    1113         151 :           LabelValue labelValue = parseLabelValue(value);
    1114         151 :           if (allValues.add(labelValue.getValue())) {
    1115         151 :             values.add(labelValue);
    1116             :           } else {
    1117           1 :             error(String.format("Duplicate %s \"%s\" for label \"%s\"", KEY_VALUE, value, name));
    1118             :           }
    1119           0 :         } catch (IllegalArgumentException notValue) {
    1120           0 :           error(
    1121           0 :               String.format(
    1122             :                   "Invalid %s \"%s\" for label \"%s\": %s",
    1123           0 :                   KEY_VALUE, value, name, notValue.getMessage()));
    1124         151 :         }
    1125             :       }
    1126             : 
    1127             :       LabelType.Builder label;
    1128             :       try {
    1129         151 :         label = LabelType.builder(name, values);
    1130           0 :       } catch (IllegalArgumentException badName) {
    1131           0 :         error(String.format("Invalid label \"%s\"", name));
    1132           0 :         continue;
    1133         151 :       }
    1134             : 
    1135         151 :       label.setDescription(Optional.ofNullable(rc.getString(LABEL, name, KEY_LABEL_DESCRIPTION)));
    1136             : 
    1137         151 :       String functionName = rc.getString(LABEL, name, KEY_FUNCTION);
    1138             :       Optional<LabelFunction> function =
    1139         151 :           functionName != null
    1140         151 :               ? LabelFunction.parse(functionName)
    1141         151 :               : Optional.of(LabelFunction.MAX_WITH_BLOCK);
    1142         151 :       if (!function.isPresent()) {
    1143           0 :         error(
    1144           0 :             String.format(
    1145             :                 "Invalid %s for label \"%s\". Valid names are: %s",
    1146           0 :                 KEY_FUNCTION, name, Joiner.on(", ").join(LabelFunction.ALL.keySet())));
    1147             :       }
    1148         151 :       label.setFunction(function.orElse(null));
    1149         151 :       label.setCopyCondition(rc.getString(LABEL, name, KEY_COPY_CONDITION));
    1150             : 
    1151         151 :       if (!values.isEmpty()) {
    1152         151 :         short dv = (short) rc.getInt(LABEL, name, KEY_DEFAULT_VALUE, 0);
    1153         151 :         if (isInRange(dv, values)) {
    1154         151 :           label.setDefaultValue(dv);
    1155             :         } else {
    1156           1 :           error(String.format("Invalid %s \"%s\" for label \"%s\"", KEY_DEFAULT_VALUE, dv, name));
    1157             :         }
    1158             :       }
    1159         151 :       label.setAllowPostSubmit(
    1160         151 :           rc.getBoolean(LABEL, name, KEY_ALLOW_POST_SUBMIT, LabelType.DEF_ALLOW_POST_SUBMIT));
    1161         151 :       label.setIgnoreSelfApproval(
    1162         151 :           rc.getBoolean(LABEL, name, KEY_IGNORE_SELF_APPROVAL, LabelType.DEF_IGNORE_SELF_APPROVAL));
    1163         151 :       label.setCanOverride(
    1164         151 :           rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, LabelType.DEF_CAN_OVERRIDE));
    1165         151 :       List<String> refPatterns = getStringListOrNull(rc, LABEL, name, KEY_BRANCH);
    1166         151 :       if (refPatterns == null) {
    1167         151 :         label.setRefPatterns(null);
    1168             :       } else {
    1169           7 :         for (String pattern : refPatterns) {
    1170           7 :           if (pattern.startsWith("^")) {
    1171             :             try {
    1172           4 :               Pattern.compile(pattern);
    1173           1 :             } catch (PatternSyntaxException e) {
    1174           1 :               error(
    1175           1 :                   String.format(
    1176             :                       "Invalid ref pattern \"%s\" in %s.%s.%s: %s",
    1177           1 :                       pattern, LABEL, name, KEY_BRANCH, e.getMessage()));
    1178           4 :             }
    1179             :           }
    1180           7 :         }
    1181           7 :         label.setRefPatterns(ImmutableList.copyOf(refPatterns));
    1182             :       }
    1183         151 :       labelSections.put(name, label.build());
    1184         151 :     }
    1185         151 :   }
    1186             : 
    1187             :   private boolean isInRange(short value, List<LabelValue> labelValues) {
    1188         151 :     for (LabelValue lv : labelValues) {
    1189         151 :       if (lv.getValue() == value) {
    1190         151 :         return true;
    1191             :       }
    1192         151 :     }
    1193           1 :     return false;
    1194             :   }
    1195             : 
    1196             :   @Nullable
    1197             :   private List<String> getStringListOrNull(
    1198             :       Config rc, String section, String subSection, String name) {
    1199         151 :     String[] ac = rc.getStringList(section, subSection, name);
    1200         151 :     return ac.length == 0 ? null : Arrays.asList(ac);
    1201             :   }
    1202             : 
    1203             :   private void loadCommentLinkSections(Config rc) {
    1204         151 :     Set<String> subsections = rc.getSubsections(COMMENTLINK);
    1205         151 :     commentLinkSections = new LinkedHashMap<>(subsections.size());
    1206         151 :     for (String name : subsections) {
    1207             :       try {
    1208           2 :         commentLinkSections.put(name, buildCommentLink(rc, name));
    1209           1 :       } catch (PatternSyntaxException e) {
    1210           1 :         error(
    1211           1 :             String.format(
    1212             :                 "Invalid pattern \"%s\" in commentlink.%s.match: %s",
    1213           1 :                 rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage()));
    1214           1 :       } catch (IllegalArgumentException e) {
    1215           1 :         error(
    1216           1 :             String.format(
    1217             :                 "Error in pattern \"%s\" in commentlink.%s.match: %s",
    1218           1 :                 rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage()));
    1219           2 :       }
    1220           2 :     }
    1221         151 :   }
    1222             : 
    1223             :   private void loadSubscribeSections(Config rc) throws ConfigInvalidException {
    1224         151 :     Set<String> subsections = rc.getSubsections(SUBSCRIBE_SECTION);
    1225         151 :     subscribeSections = new HashMap<>();
    1226             :     try {
    1227         151 :       for (String projectName : subsections) {
    1228           2 :         Project.NameKey p = Project.nameKey(projectName);
    1229           2 :         SubscribeSection.Builder ss = SubscribeSection.builder(p);
    1230             :         for (String s :
    1231           2 :             rc.getStringList(SUBSCRIBE_SECTION, projectName, SUBSCRIBE_MULTI_MATCH_REFS)) {
    1232           1 :           ss.addMultiMatchRefSpec(s);
    1233             :         }
    1234           2 :         for (String s : rc.getStringList(SUBSCRIBE_SECTION, projectName, SUBSCRIBE_MATCH_REFS)) {
    1235           2 :           ss.addMatchingRefSpec(s);
    1236             :         }
    1237           2 :         subscribeSections.put(p, ss.build());
    1238           2 :       }
    1239           0 :     } catch (IllegalArgumentException e) {
    1240           0 :       throw new ConfigInvalidException(e.getMessage());
    1241         151 :     }
    1242         151 :   }
    1243             : 
    1244             :   private void loadReceiveSection(Config rc) {
    1245         151 :     checkReceivedObjects = rc.getBoolean(RECEIVE, KEY_CHECK_RECEIVED_OBJECTS, true);
    1246         151 :     maxObjectSizeLimit = rc.getLong(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, 0);
    1247         151 :   }
    1248             : 
    1249             :   private void loadPluginSections(Config rc) {
    1250         151 :     pluginConfigs = new HashMap<>();
    1251         151 :     for (String plugin : rc.getSubsections(PLUGIN)) {
    1252           3 :       Config pluginConfig = new Config();
    1253           3 :       pluginConfigs.put(plugin, pluginConfig);
    1254           3 :       for (String name : rc.getNames(PLUGIN, plugin)) {
    1255           3 :         String value = rc.getString(PLUGIN, plugin, name);
    1256           3 :         String groupName = GroupReference.extractGroupName(value);
    1257           3 :         if (groupName != null) {
    1258           2 :           GroupReference ref = groupList.byName(groupName);
    1259           2 :           if (ref == null) {
    1260           1 :             error(String.format("group \"%s\" not in %s", groupName, GroupList.FILE_NAME));
    1261             :           }
    1262           2 :           rc.setString(PLUGIN, plugin, name, value);
    1263             :         }
    1264           3 :         pluginConfig.setStringList(
    1265           3 :             PLUGIN, plugin, name, Arrays.asList(rc.getStringList(PLUGIN, plugin, name)));
    1266           3 :       }
    1267           3 :     }
    1268         151 :   }
    1269             : 
    1270             :   public void updatePluginConfig(
    1271             :       String pluginName, Consumer<PluginConfig.Update> pluginConfigUpdate) {
    1272           3 :     Config pluginConfig = pluginConfigs.get(pluginName);
    1273           3 :     if (pluginConfig == null) {
    1274           2 :       pluginConfig = new Config();
    1275           2 :       pluginConfigs.put(pluginName, pluginConfig);
    1276             :     }
    1277           3 :     pluginConfigUpdate.accept(new PluginConfig.Update(pluginName, pluginConfig, Optional.of(this)));
    1278           3 :   }
    1279             : 
    1280             :   public PluginConfig getPluginConfig(String pluginName) {
    1281           2 :     Config pluginConfig = pluginConfigs.getOrDefault(pluginName, new Config());
    1282           2 :     return PluginConfig.create(pluginName, pluginConfig, getCacheable());
    1283             :   }
    1284             : 
    1285             :   private void loadProjectLevelConfigs() throws IOException {
    1286         151 :     projectLevelConfigs = new HashMap<>();
    1287         151 :     if (revision == null) {
    1288         151 :       return;
    1289             :     }
    1290         151 :     for (PathInfo pathInfo : getPathInfos(true)) {
    1291         151 :       if (pathInfo.path.endsWith(".config") && !PROJECT_CONFIG.equals(pathInfo.path)) {
    1292           2 :         String cfg = readUTF8(pathInfo.path);
    1293           2 :         Config parsedConfig = new Config();
    1294             :         try {
    1295           2 :           parsedConfig.fromText(cfg);
    1296           2 :           projectLevelConfigs.put(pathInfo.path, parsedConfig);
    1297           1 :         } catch (ConfigInvalidException e) {
    1298           1 :           logger.atWarning().withCause(e).log("Unable to parse config");
    1299           2 :         }
    1300             :       }
    1301         151 :     }
    1302         151 :   }
    1303             : 
    1304             :   private void readGroupList() throws IOException {
    1305         151 :     groupList = GroupList.parse(projectName, readUTF8(GroupList.FILE_NAME), this);
    1306         151 :   }
    1307             : 
    1308             :   @Override
    1309             :   protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
    1310         151 :     if (commit.getMessage() == null || "".equals(commit.getMessage())) {
    1311          85 :       commit.setMessage("Updated project configuration\n");
    1312             :     }
    1313             : 
    1314         151 :     Config rc = readConfig(PROJECT_CONFIG);
    1315         151 :     Project p = project;
    1316             : 
    1317         151 :     if (p.getDescription() != null && !p.getDescription().isEmpty()) {
    1318         151 :       rc.setString(PROJECT, null, KEY_DESCRIPTION, p.getDescription());
    1319             :     } else {
    1320         144 :       rc.unset(PROJECT, null, KEY_DESCRIPTION);
    1321             :     }
    1322         151 :     set(rc, ACCESS, null, KEY_INHERIT_FROM, p.getParentName());
    1323             : 
    1324         151 :     for (BooleanProjectConfig config : BooleanProjectConfig.values()) {
    1325         151 :       set(
    1326             :           rc,
    1327         151 :           config.getSection(),
    1328         151 :           config.getSubSection(),
    1329         151 :           config.getName(),
    1330         151 :           p.getBooleanConfig(config),
    1331             :           InheritableBoolean.INHERIT);
    1332             :     }
    1333             : 
    1334         151 :     set(
    1335             :         rc,
    1336             :         RECEIVE,
    1337             :         null,
    1338             :         KEY_MAX_OBJECT_SIZE_LIMIT,
    1339         151 :         validMaxObjectSizeLimit(p.getMaxObjectSizeLimit()));
    1340             : 
    1341         151 :     set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), DEFAULT_SUBMIT_TYPE);
    1342             : 
    1343         151 :     set(rc, PROJECT, null, KEY_STATE, p.getState(), DEFAULT_STATE_VALUE);
    1344             : 
    1345         151 :     set(rc, DASHBOARD, null, KEY_DEFAULT, p.getDefaultDashboard());
    1346         151 :     set(rc, DASHBOARD, null, KEY_LOCAL_DEFAULT, p.getLocalDefaultDashboard());
    1347             : 
    1348         151 :     Set<AccountGroup.UUID> keepGroups = new HashSet<>();
    1349         151 :     saveAccountsSection(rc, keepGroups);
    1350         151 :     saveContributorAgreements(rc, keepGroups);
    1351         151 :     saveAccessSections(rc, keepGroups);
    1352         151 :     saveNotifySections(rc, keepGroups);
    1353         151 :     savePluginSections(rc, keepGroups);
    1354         151 :     groupList.retainUUIDs(keepGroups);
    1355         151 :     saveLabelSections(rc);
    1356         151 :     saveSubmitRequirementSections(rc);
    1357         151 :     saveCommentLinkSections(rc);
    1358         151 :     saveSubscribeSections(rc);
    1359         151 :     saveBranchOrderSection(rc);
    1360             : 
    1361         151 :     saveConfig(PROJECT_CONFIG, rc);
    1362         151 :     saveGroupList();
    1363         151 :     return true;
    1364             :   }
    1365             : 
    1366             :   @Nullable
    1367             :   public static String validMaxObjectSizeLimit(String value) throws ConfigInvalidException {
    1368         151 :     if (value == null) {
    1369         151 :       return null;
    1370             :     }
    1371           1 :     value = value.trim();
    1372           1 :     if (value.isEmpty()) {
    1373           0 :       return null;
    1374             :     }
    1375           1 :     Config cfg = new Config();
    1376           1 :     cfg.fromText("[s]\nn=" + value);
    1377             :     try {
    1378           1 :       long s = cfg.getLong("s", "n", 0);
    1379           1 :       if (s < 0) {
    1380           0 :         throw new ConfigInvalidException(
    1381           0 :             String.format(
    1382             :                 "Negative value '%s' not allowed as %s", value, KEY_MAX_OBJECT_SIZE_LIMIT));
    1383             :       }
    1384           1 :       if (s == 0) {
    1385             :         // return null for the default so that it is not persisted
    1386           1 :         return null;
    1387             :       }
    1388           1 :       return value;
    1389           1 :     } catch (IllegalArgumentException e) {
    1390           1 :       throw new ConfigInvalidException(
    1391           1 :           String.format("Value '%s' not parseable as a Long", value), e);
    1392             :     }
    1393             :   }
    1394             : 
    1395             :   private void saveAccountsSection(Config rc, Set<AccountGroup.UUID> keepGroups) {
    1396         151 :     unsetSection(rc, ACCOUNTS);
    1397         151 :     if (accountsSection != null) {
    1398         151 :       rc.setStringList(
    1399             :           ACCOUNTS,
    1400             :           null,
    1401             :           KEY_SAME_GROUP_VISIBILITY,
    1402         151 :           ruleToStringList(accountsSection.getSameGroupVisibility(), keepGroups));
    1403             :     }
    1404         151 :   }
    1405             : 
    1406             :   private void saveCommentLinkSections(Config rc) {
    1407         151 :     unsetSection(rc, COMMENTLINK);
    1408         151 :     if (commentLinkSections != null) {
    1409         151 :       for (StoredCommentLinkInfo cm : commentLinkSections.values()) {
    1410             :         // Match and Link can be empty if the commentlink is override only.
    1411           2 :         if (!Strings.isNullOrEmpty(cm.getMatch())) {
    1412           2 :           rc.setString(COMMENTLINK, cm.getName(), KEY_MATCH, cm.getMatch());
    1413             :         }
    1414           2 :         if (!Strings.isNullOrEmpty(cm.getLink())) {
    1415           2 :           rc.setString(COMMENTLINK, cm.getName(), KEY_LINK, cm.getLink());
    1416             :         }
    1417           2 :         if (!Strings.isNullOrEmpty(cm.getPrefix())) {
    1418           1 :           rc.setString(COMMENTLINK, cm.getName(), KEY_PREFIX, cm.getPrefix());
    1419             :         }
    1420           2 :         if (!Strings.isNullOrEmpty(cm.getSuffix())) {
    1421           1 :           rc.setString(COMMENTLINK, cm.getName(), KEY_SUFFIX, cm.getSuffix());
    1422             :         }
    1423           2 :         if (!Strings.isNullOrEmpty(cm.getText())) {
    1424           1 :           rc.setString(COMMENTLINK, cm.getName(), KEY_TEXT, cm.getText());
    1425             :         }
    1426           2 :         if (cm.getEnabled() != null && !cm.getEnabled()) {
    1427           0 :           rc.setBoolean(COMMENTLINK, cm.getName(), KEY_ENABLED, cm.getEnabled());
    1428             :         }
    1429           2 :       }
    1430             :     }
    1431         151 :   }
    1432             : 
    1433             :   private void saveContributorAgreements(Config rc, Set<AccountGroup.UUID> keepGroups) {
    1434         151 :     unsetSection(rc, CONTRIBUTOR_AGREEMENT);
    1435         151 :     for (ContributorAgreement ca : sort(contributorAgreements.values())) {
    1436           2 :       set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_DESCRIPTION, ca.getDescription());
    1437           2 :       set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AGREEMENT_URL, ca.getAgreementUrl());
    1438             : 
    1439           2 :       if (ca.getAutoVerify() != null) {
    1440           1 :         if (ca.getAutoVerify().getUUID() != null) {
    1441           1 :           keepGroups.add(ca.getAutoVerify().getUUID());
    1442             :         }
    1443           1 :         String autoVerify = PermissionRule.create(ca.getAutoVerify()).asString(false);
    1444           1 :         set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AUTO_VERIFY, autoVerify);
    1445           1 :       } else {
    1446           2 :         rc.unset(CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AUTO_VERIFY);
    1447             :       }
    1448             : 
    1449           2 :       rc.setStringList(
    1450             :           CONTRIBUTOR_AGREEMENT,
    1451           2 :           ca.getName(),
    1452             :           KEY_ACCEPTED,
    1453           2 :           ruleToStringList(ca.getAccepted(), keepGroups));
    1454           2 :       rc.setStringList(
    1455             :           CONTRIBUTOR_AGREEMENT,
    1456           2 :           ca.getName(),
    1457             :           KEY_EXCLUDE_PROJECTS,
    1458           2 :           patternToStringList(ca.getExcludeProjectsRegexes()));
    1459           2 :       rc.setStringList(
    1460             :           CONTRIBUTOR_AGREEMENT,
    1461           2 :           ca.getName(),
    1462             :           KEY_MATCH_PROJECTS,
    1463           2 :           patternToStringList(ca.getMatchProjectsRegexes()));
    1464           2 :     }
    1465         151 :   }
    1466             : 
    1467             :   private void saveNotifySections(Config rc, Set<AccountGroup.UUID> keepGroups) {
    1468         151 :     unsetSection(rc, NOTIFY);
    1469         151 :     for (NotifyConfig nc : sort(notifySections.values())) {
    1470           2 :       nc.getGroups().stream()
    1471           2 :           .map(GroupReference::getUUID)
    1472           2 :           .filter(Objects::nonNull)
    1473           2 :           .forEach(keepGroups::add);
    1474           2 :       List<String> email =
    1475           2 :           nc.getGroups().stream()
    1476           2 :               .map(gr -> PermissionRule.create(gr).asString(false))
    1477           2 :               .sorted()
    1478           2 :               .collect(toList());
    1479             : 
    1480             :       // Separate stream operation so that emails list contains 2 sorted sub-lists.
    1481           2 :       nc.getAddresses().stream().map(Address::toString).sorted().forEach(email::add);
    1482             : 
    1483           2 :       set(rc, NOTIFY, nc.getName(), KEY_HEADER, nc.getHeader(), NotifyConfig.Header.BCC);
    1484           2 :       if (email.isEmpty()) {
    1485           0 :         rc.unset(NOTIFY, nc.getName(), KEY_EMAIL);
    1486             :       } else {
    1487           2 :         rc.setStringList(NOTIFY, nc.getName(), KEY_EMAIL, email);
    1488             :       }
    1489             : 
    1490           2 :       if (nc.getNotify().equals(Sets.immutableEnumSet(NotifyType.ALL))) {
    1491           1 :         rc.unset(NOTIFY, nc.getName(), KEY_TYPE);
    1492             :       } else {
    1493           1 :         List<String> types = new ArrayList<>(4);
    1494           1 :         for (NotifyType t : NotifyType.values()) {
    1495           1 :           if (nc.isNotify(t)) {
    1496           1 :             types.add(t.name().toLowerCase(Locale.US));
    1497             :           }
    1498             :         }
    1499           1 :         rc.setStringList(NOTIFY, nc.getName(), KEY_TYPE, types);
    1500             :       }
    1501             : 
    1502           2 :       set(rc, NOTIFY, nc.getName(), KEY_FILTER, nc.getFilter());
    1503           2 :     }
    1504         151 :   }
    1505             : 
    1506             :   private List<String> patternToStringList(List<String> list) {
    1507           2 :     return list;
    1508             :   }
    1509             : 
    1510             :   private List<String> ruleToStringList(
    1511             :       List<PermissionRule> list, Set<AccountGroup.UUID> keepGroups) {
    1512         151 :     List<String> rules = new ArrayList<>();
    1513         151 :     for (PermissionRule rule : sort(list)) {
    1514           2 :       if (rule.getGroup().getUUID() != null) {
    1515           2 :         keepGroups.add(rule.getGroup().getUUID());
    1516             :       }
    1517           2 :       rules.add(rule.asString(false));
    1518           2 :     }
    1519         151 :     return rules;
    1520             :   }
    1521             : 
    1522             :   private void saveAccessSections(Config rc, Set<AccountGroup.UUID> keepGroups) {
    1523         151 :     unsetSection(rc, CAPABILITY);
    1524         151 :     AccessSection capability = accessSections.get(AccessSection.GLOBAL_CAPABILITIES);
    1525         151 :     if (capability != null) {
    1526         151 :       Set<String> have = new HashSet<>();
    1527         151 :       for (Permission permission : sort(capability.getPermissions())) {
    1528         151 :         have.add(permission.getName().toLowerCase());
    1529             : 
    1530         151 :         boolean needRange = GlobalCapability.hasRange(permission.getName());
    1531         151 :         List<String> rules = new ArrayList<>();
    1532         151 :         for (PermissionRule rule : sort(permission.getRules())) {
    1533         151 :           GroupReference group = resolve(rule.getGroup());
    1534         151 :           if (group.getUUID() != null) {
    1535         151 :             keepGroups.add(group.getUUID());
    1536             :           }
    1537         151 :           rules.add(rule.toBuilder().setGroup(group).build().asString(needRange));
    1538         151 :         }
    1539         151 :         rc.setStringList(CAPABILITY, null, permission.getName(), rules);
    1540         151 :       }
    1541         151 :       for (String varName : rc.getNames(CAPABILITY)) {
    1542         151 :         if (!have.contains(varName.toLowerCase())) {
    1543           0 :           rc.unset(CAPABILITY, null, varName);
    1544             :         }
    1545         151 :       }
    1546         151 :     } else {
    1547         151 :       rc.unsetSection(CAPABILITY, null);
    1548             :     }
    1549             : 
    1550         151 :     for (AccessSection as : sort(accessSections.values())) {
    1551         151 :       String refName = as.getName();
    1552         151 :       if (AccessSection.GLOBAL_CAPABILITIES.equals(refName)) {
    1553         151 :         continue;
    1554             :       }
    1555             : 
    1556         151 :       StringBuilder doNotInherit = new StringBuilder();
    1557         151 :       for (Permission perm : sort(as.getPermissions())) {
    1558         151 :         if (perm.getExclusiveGroup()) {
    1559         151 :           if (0 < doNotInherit.length()) {
    1560         151 :             doNotInherit.append(' ');
    1561             :           }
    1562         151 :           doNotInherit.append(perm.getName());
    1563             :         }
    1564         151 :       }
    1565         151 :       if (0 < doNotInherit.length()) {
    1566         151 :         rc.setString(ACCESS, refName, KEY_GROUP_PERMISSIONS, doNotInherit.toString());
    1567             :       } else {
    1568         151 :         rc.unset(ACCESS, refName, KEY_GROUP_PERMISSIONS);
    1569             :       }
    1570             : 
    1571         151 :       Set<String> have = new HashSet<>();
    1572         151 :       for (Permission permission : sort(as.getPermissions())) {
    1573         151 :         have.add(permission.getName().toLowerCase());
    1574             : 
    1575         151 :         boolean needRange = Permission.hasRange(permission.getName());
    1576         151 :         List<String> rules = new ArrayList<>();
    1577         151 :         for (PermissionRule rule : sort(permission.getRules())) {
    1578         151 :           GroupReference group = resolve(rule.getGroup());
    1579         151 :           if (group.getUUID() != null) {
    1580         151 :             keepGroups.add(group.getUUID());
    1581             :           }
    1582         151 :           rules.add(rule.toBuilder().setGroup(group).build().asString(needRange));
    1583         151 :         }
    1584         151 :         rc.setStringList(ACCESS, refName, permission.getName(), rules);
    1585         151 :       }
    1586             : 
    1587         151 :       for (String varName : rc.getNames(ACCESS, refName)) {
    1588         151 :         if (isCoreOrPluginPermission(convertLegacyPermission(varName))
    1589         151 :             && !have.contains(varName.toLowerCase())) {
    1590          10 :           rc.unset(ACCESS, refName, varName);
    1591             :         }
    1592         151 :       }
    1593         151 :     }
    1594             : 
    1595         151 :     for (String name : rc.getSubsections(ACCESS)) {
    1596         151 :       if (AccessSection.isValidRefSectionName(name) && !accessSections.containsKey(name)) {
    1597           3 :         rc.unsetSection(ACCESS, name);
    1598             :       }
    1599         151 :     }
    1600         151 :   }
    1601             : 
    1602             :   private void saveLabelSections(Config rc) {
    1603         151 :     List<String> existing = new ArrayList<>(rc.getSubsections(LABEL));
    1604         151 :     if (!new ArrayList<>(labelSections.keySet()).equals(existing)) {
    1605             :       // Order of sections changed, remove and rewrite them all.
    1606         151 :       unsetSection(rc, LABEL);
    1607             :     }
    1608             : 
    1609         151 :     Set<String> toUnset = new HashSet<>(existing);
    1610         151 :     for (Map.Entry<String, LabelType> e : labelSections.entrySet()) {
    1611         151 :       String name = e.getKey();
    1612         151 :       LabelType label = e.getValue();
    1613         151 :       toUnset.remove(name);
    1614         151 :       if (label.getDescription().isPresent() && !label.getDescription().get().isEmpty()) {
    1615          11 :         rc.setString(LABEL, name, KEY_LABEL_DESCRIPTION, label.getDescription().get());
    1616             :       } else {
    1617         151 :         rc.unset(LABEL, name, KEY_LABEL_DESCRIPTION);
    1618             :       }
    1619         151 :       rc.setString(LABEL, name, KEY_FUNCTION, label.getFunction().getFunctionName());
    1620         151 :       rc.setInt(LABEL, name, KEY_DEFAULT_VALUE, label.getDefaultValue());
    1621             : 
    1622         151 :       setBooleanConfigKey(
    1623             :           rc,
    1624             :           LABEL,
    1625             :           name,
    1626             :           KEY_ALLOW_POST_SUBMIT,
    1627         151 :           label.isAllowPostSubmit(),
    1628             :           LabelType.DEF_ALLOW_POST_SUBMIT);
    1629         151 :       setBooleanConfigKey(
    1630             :           rc,
    1631             :           LABEL,
    1632             :           name,
    1633             :           KEY_IGNORE_SELF_APPROVAL,
    1634         151 :           label.isIgnoreSelfApproval(),
    1635             :           LabelType.DEF_IGNORE_SELF_APPROVAL);
    1636         151 :       setBooleanConfigKey(
    1637         151 :           rc, LABEL, name, KEY_CAN_OVERRIDE, label.isCanOverride(), LabelType.DEF_CAN_OVERRIDE);
    1638         151 :       List<String> values = new ArrayList<>(label.getValues().size());
    1639         151 :       for (LabelValue value : label.getValues()) {
    1640         151 :         values.add(value.format().trim());
    1641         151 :       }
    1642         151 :       rc.setStringList(LABEL, name, KEY_VALUE, values);
    1643         151 :       if (label.getCopyCondition().isPresent()) {
    1644         151 :         rc.setString(LABEL, name, KEY_COPY_CONDITION, label.getCopyCondition().get());
    1645             :       } else {
    1646          27 :         rc.unset(LABEL, name, KEY_COPY_CONDITION);
    1647             :       }
    1648             : 
    1649         151 :       List<String> refPatterns = label.getRefPatterns();
    1650         151 :       if (refPatterns != null && !refPatterns.isEmpty()) {
    1651           6 :         rc.setStringList(LABEL, name, KEY_BRANCH, refPatterns);
    1652             :       } else {
    1653         151 :         rc.unset(LABEL, name, KEY_BRANCH);
    1654             :       }
    1655         151 :     }
    1656             : 
    1657         151 :     for (String name : toUnset) {
    1658           5 :       rc.unsetSection(LABEL, name);
    1659           5 :     }
    1660         151 :   }
    1661             : 
    1662             :   private void saveSubmitRequirementSections(Config rc) {
    1663         151 :     unsetSection(rc, SUBMIT_REQUIREMENT);
    1664             : 
    1665         151 :     if (submitRequirementSections != null) {
    1666         151 :       for (Map.Entry<String, SubmitRequirement> entry : submitRequirementSections.entrySet()) {
    1667           5 :         String name = entry.getKey();
    1668           5 :         SubmitRequirement sr = entry.getValue();
    1669             : 
    1670           5 :         if (sr.description().isPresent()) {
    1671           2 :           rc.setString(SUBMIT_REQUIREMENT, name, KEY_SR_DESCRIPTION, sr.description().get());
    1672             :         }
    1673           5 :         if (sr.applicabilityExpression().isPresent()) {
    1674           3 :           rc.setString(
    1675             :               SUBMIT_REQUIREMENT,
    1676             :               name,
    1677             :               KEY_SR_APPLICABILITY_EXPRESSION,
    1678           3 :               sr.applicabilityExpression().get().expressionString());
    1679             :         }
    1680           5 :         rc.setString(
    1681             :             SUBMIT_REQUIREMENT,
    1682             :             name,
    1683             :             KEY_SR_SUBMITTABILITY_EXPRESSION,
    1684           5 :             sr.submittabilityExpression().expressionString());
    1685           5 :         if (sr.overrideExpression().isPresent()) {
    1686           2 :           rc.setString(
    1687             :               SUBMIT_REQUIREMENT,
    1688             :               name,
    1689             :               KEY_SR_OVERRIDE_EXPRESSION,
    1690           2 :               sr.overrideExpression().get().expressionString());
    1691             :         }
    1692           5 :         rc.setBoolean(
    1693             :             SUBMIT_REQUIREMENT,
    1694             :             name,
    1695             :             KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
    1696           5 :             sr.allowOverrideInChildProjects());
    1697           5 :       }
    1698             :     }
    1699         151 :   }
    1700             : 
    1701             :   private static void setBooleanConfigKey(
    1702             :       Config rc, String section, String name, String key, boolean value, boolean defaultValue) {
    1703         151 :     if (value == defaultValue) {
    1704         151 :       rc.unset(section, name, key);
    1705             :     } else {
    1706           8 :       rc.setBoolean(section, name, key, value);
    1707             :     }
    1708         151 :   }
    1709             : 
    1710             :   private void savePluginSections(Config rc, Set<AccountGroup.UUID> keepGroups) {
    1711         151 :     unsetSection(rc, PLUGIN);
    1712         151 :     for (Map.Entry<String, Config> e : pluginConfigs.entrySet()) {
    1713           3 :       String plugin = e.getKey();
    1714           3 :       Config pluginConfig = e.getValue();
    1715           3 :       for (String name : pluginConfig.getNames(PLUGIN, plugin)) {
    1716           3 :         String value = pluginConfig.getString(PLUGIN, plugin, name);
    1717           3 :         String groupName = GroupReference.extractGroupName(value);
    1718           3 :         if (groupName != null) {
    1719           2 :           GroupReference ref = groupList.byName(groupName);
    1720           2 :           if (ref != null && ref.getUUID() != null) {
    1721           2 :             keepGroups.add(ref.getUUID());
    1722           2 :             pluginConfig.setString(PLUGIN, plugin, name, "group " + ref.getName());
    1723             :           }
    1724             :         }
    1725           3 :         rc.setStringList(
    1726           3 :             PLUGIN, plugin, name, Arrays.asList(pluginConfig.getStringList(PLUGIN, plugin, name)));
    1727           3 :       }
    1728           3 :     }
    1729         151 :   }
    1730             : 
    1731             :   private void saveGroupList() throws IOException {
    1732         151 :     saveUTF8(GroupList.FILE_NAME, groupList.asText());
    1733         151 :   }
    1734             : 
    1735             :   private void saveSubscribeSections(Config rc) {
    1736         151 :     for (Project.NameKey p : subscribeSections.keySet()) {
    1737           2 :       SubscribeSection s = subscribeSections.get(p);
    1738           2 :       List<String> matchings = new ArrayList<>();
    1739           2 :       for (String r : s.matchingRefSpecsAsString()) {
    1740           2 :         matchings.add(r);
    1741           2 :       }
    1742           2 :       rc.setStringList(SUBSCRIBE_SECTION, p.get(), SUBSCRIBE_MATCH_REFS, matchings);
    1743             : 
    1744           2 :       List<String> multimatchs = new ArrayList<>();
    1745           2 :       for (String r : s.multiMatchRefSpecsAsString()) {
    1746           1 :         multimatchs.add(r);
    1747           1 :       }
    1748           2 :       rc.setStringList(SUBSCRIBE_SECTION, p.get(), SUBSCRIBE_MULTI_MATCH_REFS, multimatchs);
    1749           2 :     }
    1750         151 :   }
    1751             : 
    1752             :   private void unsetSection(Config rc, String sectionName) {
    1753         151 :     for (String subSectionName : rc.getSubsections(sectionName)) {
    1754          15 :       rc.unsetSection(sectionName, subSectionName);
    1755          15 :     }
    1756         151 :     rc.unsetSection(sectionName, null);
    1757         151 :   }
    1758             : 
    1759             :   private <E extends Enum<?>> E getEnum(
    1760             :       Config rc, String section, String subsection, String name, E defaultValue) {
    1761             :     try {
    1762         151 :       return rc.getEnum(section, subsection, name, defaultValue);
    1763           0 :     } catch (IllegalArgumentException err) {
    1764           0 :       error(err.getMessage());
    1765           0 :       return defaultValue;
    1766             :     }
    1767             :   }
    1768             : 
    1769             :   private void error(String errorMessage) {
    1770           5 :     error(ValidationError.create(PROJECT_CONFIG, errorMessage));
    1771           5 :   }
    1772             : 
    1773             :   @Override
    1774             :   public void error(ValidationError error) {
    1775           5 :     if (validationErrors == null) {
    1776           5 :       validationErrors = new ArrayList<>(4);
    1777             :     }
    1778           5 :     validationErrors.add(error);
    1779           5 :   }
    1780             : 
    1781             :   private static <T extends Comparable<? super T>> ImmutableList<T> sort(Collection<T> m) {
    1782         151 :     return m.stream().sorted().collect(toImmutableList());
    1783             :   }
    1784             : 
    1785             :   @UsedAt(UsedAt.Project.GOOGLE)
    1786             :   public boolean hasLegacyPermissions() {
    1787           0 :     return hasLegacyPermissions;
    1788             :   }
    1789             : 
    1790             :   private String convertLegacyPermission(String permissionName) {
    1791         151 :     switch (permissionName) {
    1792             :       case LEGACY_PERMISSION_PUSH_TAG:
    1793           0 :         hasLegacyPermissions = true;
    1794           0 :         return Permission.CREATE_TAG;
    1795             :       case LEGACY_PERMISSION_PUSH_SIGNED_TAG:
    1796           0 :         hasLegacyPermissions = true;
    1797           0 :         return Permission.CREATE_SIGNED_TAG;
    1798             :       default:
    1799         151 :         return permissionName;
    1800             :     }
    1801             :   }
    1802             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750