All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.project.ProjectConfig Maven / Gradle / Ivy

There is a newer version: 3.10.1
Show newest version
// Copyright (C) 2010 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.server.project;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.entities.Permission.isPermission;
import static com.google.gerrit.entities.Project.DEFAULT_SUBMIT_TYPE;
import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isValidPluginPermission;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Shorts;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.AccountsSection;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchOrderSection;
import com.google.gerrit.entities.CachedProjectConfig;
import com.google.gerrit.entities.ConfiguredMimeTypes;
import com.google.gerrit.entities.ContributorAgreement;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.GroupReference;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelValue;
import com.google.gerrit.entities.NotifyConfig;
import com.google.gerrit.entities.NotifyConfig.NotifyType;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.PermissionRule;
import com.google.gerrit.entities.PermissionRule.Action;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.StoredCommentLinkInfo;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubscribeSection;
import com.google.gerrit.exceptions.InvalidNameException;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.config.AllProjectsConfigProvider;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.git.ValidationError;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.git.meta.VersionedMetaData;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevWalk;

public class ProjectConfig extends VersionedMetaData implements ValidationError.Sink {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  public static final String COMMENTLINK = "commentlink";
  public static final String LABEL = "label";
  public static final String KEY_LABEL_DESCRIPTION = "description";
  public static final String KEY_FUNCTION = "function";
  public static final String KEY_DEFAULT_VALUE = "defaultValue";
  public static final String KEY_ALLOW_POST_SUBMIT = "allowPostSubmit";
  public static final String KEY_IGNORE_SELF_APPROVAL = "ignoreSelfApproval";
  public static final String KEY_COPY_CONDITION = "copyCondition";
  public static final String KEY_VALUE = "value";
  public static final String KEY_CAN_OVERRIDE = "canOverride";
  public static final String KEY_BRANCH = "branch";

  public static final String SUBMIT_REQUIREMENT = "submit-requirement";
  public static final String KEY_SR_DESCRIPTION = "description";
  public static final String KEY_SR_APPLICABILITY_EXPRESSION = "applicableIf";
  public static final String KEY_SR_SUBMITTABILITY_EXPRESSION = "submittableIf";
  public static final String KEY_SR_OVERRIDE_EXPRESSION = "overrideIf";
  public static final String KEY_SR_OVERRIDE_IN_CHILD_PROJECTS = "canOverrideInChildProjects";
  public static final ImmutableSet SR_KEYS =
      ImmutableSet.of(
          KEY_SR_DESCRIPTION,
          KEY_SR_APPLICABILITY_EXPRESSION,
          KEY_SR_SUBMITTABILITY_EXPRESSION,
          KEY_SR_OVERRIDE_EXPRESSION,
          KEY_SR_OVERRIDE_IN_CHILD_PROJECTS);

  public static final String KEY_MATCH = "match";
  private static final String KEY_HTML = "html";
  public static final String KEY_LINK = "link";
  public static final String KEY_PREFIX = "prefix";
  public static final String KEY_SUFFIX = "suffix";
  public static final String KEY_TEXT = "text";
  public static final String KEY_ENABLED = "enabled";

  public static final String PROJECT_CONFIG = "project.config";

  private static final String PROJECT = "project";
  private static final String KEY_DESCRIPTION = "description";

  public static final String ACCESS = "access";
  private static final String KEY_INHERIT_FROM = "inheritFrom";
  private static final String KEY_GROUP_PERMISSIONS = "exclusiveGroupPermissions";

  private static final String ACCOUNTS = "accounts";
  private static final String KEY_SAME_GROUP_VISIBILITY = "sameGroupVisibility";

  private static final String BRANCH_ORDER = "branchOrder";
  private static final String BRANCH = "branch";

  private static final String CONTRIBUTOR_AGREEMENT = "contributor-agreement";
  private static final String KEY_ACCEPTED = "accepted";
  private static final String KEY_AUTO_VERIFY = "autoVerify";
  private static final String KEY_AGREEMENT_URL = "agreementUrl";
  private static final String KEY_MATCH_PROJECTS = "matchProjects";
  private static final String KEY_EXCLUDE_PROJECTS = "excludeProjects";

  private static final String NOTIFY = "notify";
  private static final String KEY_EMAIL = "email";
  private static final String KEY_FILTER = "filter";
  private static final String KEY_TYPE = "type";
  private static final String KEY_HEADER = "header";

  private static final String CAPABILITY = "capability";

  private static final String RECEIVE = "receive";
  private static final String KEY_CHECK_RECEIVED_OBJECTS = "checkReceivedObjects";

  private static final String SUBMIT = "submit";
  private static final String KEY_ACTION = "action";
  private static final String KEY_STATE = "state";

  private static final String KEY_MAX_OBJECT_SIZE_LIMIT = "maxObjectSizeLimit";

  private static final String SUBSCRIBE_SECTION = "allowSuperproject";
  private static final String SUBSCRIBE_MATCH_REFS = "matching";
  private static final String SUBSCRIBE_MULTI_MATCH_REFS = "all";

  private static final String DASHBOARD = "dashboard";
  private static final String KEY_DEFAULT = "default";
  private static final String KEY_LOCAL_DEFAULT = "local-default";

  private static final String LEGACY_PERMISSION_PUSH_TAG = "pushTag";
  private static final String LEGACY_PERMISSION_PUSH_SIGNED_TAG = "pushSignedTag";

  private static final String PLUGIN = "plugin";

  private static final ProjectState DEFAULT_STATE_VALUE = ProjectState.ACTIVE;

  private static final String EXTENSION_PANELS = "extension-panels";
  private static final String KEY_PANEL = "panel";

  private static final Pattern EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN = Pattern.compile("[, \t]{1,}");

  // Don't use an assisted factory, since instances created by an assisted factory retain references
  // to their enclosing injector. Instances of ProjectConfig are cached for a long time in the
  // ProjectCache, so this would retain lots more memory.
  @Singleton
  public static class Factory {
    private final AllProjectsName allProjectsName;
    private final AllProjectsConfigProvider allProjectsConfigProvider;

    @Inject
    Factory(AllProjectsName allProjectsName, AllProjectsConfigProvider allProjectsConfigProvider) {
      this.allProjectsName = allProjectsName;
      this.allProjectsConfigProvider = allProjectsConfigProvider;
    }

    public ProjectConfig create(Project.NameKey projectName) {
      return new ProjectConfig(
          projectName,
          projectName.equals(allProjectsName)
              ? allProjectsConfigProvider.get(allProjectsName)
              : Optional.empty(),
          allProjectsName);
    }

    public ProjectConfig read(MetaDataUpdate update) throws IOException, ConfigInvalidException {
      ProjectConfig r = create(update.getProjectName());
      r.load(update);
      return r;
    }

    public ProjectConfig read(MetaDataUpdate update, ObjectId id)
        throws IOException, ConfigInvalidException {
      ProjectConfig r = create(update.getProjectName());
      r.load(update, id);
      return r;
    }

    @UsedAt(UsedAt.Project.COLLABNET)
    public ProjectConfig read(Repository repo, Project.NameKey name)
        throws IOException, ConfigInvalidException {
      ProjectConfig r = create(name);
      r.load(repo);
      return r;
    }
  }

  private final Optional baseConfig;
  private final AllProjectsName allProjectsName;

  private Project project;
  private AccountsSection accountsSection;
  private GroupList groupList;
  private Map accessSections;
  private BranchOrderSection branchOrderSection;
  private Map contributorAgreements;
  private Map notifySections;
  private Map labelSections;
  private Map submitRequirementSections;
  private ConfiguredMimeTypes mimeTypes;
  private Map subscribeSections;
  private Map commentLinkSections;
  private List validationErrors;
  private ObjectId rulesId;
  private long maxObjectSizeLimit;
  private Map pluginConfigs;
  private Map projectLevelConfigs;
  private boolean checkReceivedObjects;
  private Set sectionsWithUnknownPermissions;
  private boolean hasLegacyPermissions;
  private Map> extensionPanelSections;

  /** Returns an immutable, thread-safe representation of this object that can be cached. */
  public CachedProjectConfig getCacheable() {
    CachedProjectConfig.Builder builder =
        CachedProjectConfig.builder()
            .setProject(project)
            .setAccountsSection(accountsSection)
            .setBranchOrderSection(Optional.ofNullable(branchOrderSection))
            .setMimeTypes(mimeTypes)
            .setRulesId(Optional.ofNullable(rulesId))
            .setRevision(Optional.ofNullable(getRevision()))
            .setMaxObjectSizeLimit(maxObjectSizeLimit)
            .setCheckReceivedObjects(checkReceivedObjects)
            .setExtensionPanelSections(extensionPanelSections);
    groupList.byUUID().values().forEach(g -> builder.addGroup(g));
    contributorAgreements.values().forEach(c -> builder.addContributorAgreement(c));
    notifySections.values().forEach(n -> builder.addNotifySection(n));
    subscribeSections.values().forEach(s -> builder.addSubscribeSection(s));
    commentLinkSections.values().forEach(c -> builder.addCommentLinkSection(c));
    labelSections.values().forEach(l -> builder.addLabelSection(l));
    submitRequirementSections.values().forEach(sr -> builder.addSubmitRequirementSection(sr));
    pluginConfigs
        .entrySet()
        .forEach(c -> builder.addPluginConfig(c.getKey(), c.getValue().toText()));
    projectLevelConfigs
        .entrySet()
        .forEach(c -> builder.addProjectLevelConfig(c.getKey(), c.getValue().toText()));

    if (projectName.equals(allProjectsName)) {
      // Filter out permissions that aren't allowed to be set on All-Projects
      accessSections
          .values()
          .forEach(
              a -> {
                List copy = new ArrayList<>();
                for (Permission p : a.getPermissions()) {
                  if (Permission.canBeOnAllProjects(a.getName(), p.getName())) {
                    copy.add(p.toBuilder());
                  }
                }
                AccessSection section =
                    AccessSection.builder(a.getName())
                        .modifyPermissions(permissions -> permissions.addAll(copy))
                        .build();
                builder.addAccessSection(section);
              });
    } else {
      accessSections.values().forEach(a -> builder.addAccessSection(a));
    }
    return builder.build();
  }

  public static StoredCommentLinkInfo buildCommentLink(Config cfg, String name, boolean allowRaw)
      throws IllegalArgumentException {
    String match = cfg.getString(COMMENTLINK, name, KEY_MATCH);
    if (match != null) {
      // Unfortunately this validation isn't entirely complete. Clients
      // can have exceptions trying to evaluate the pattern if they don't
      // support a token used, even if the server does support the token.
      //
      // At the minimum, we can trap problems related to unmatched groups.
      Pattern.compile(match);
    }

    String link = cfg.getString(COMMENTLINK, name, KEY_LINK);
    String linkPrefix = cfg.getString(COMMENTLINK, name, KEY_PREFIX);
    String linkSuffix = cfg.getString(COMMENTLINK, name, KEY_SUFFIX);
    String linkText = cfg.getString(COMMENTLINK, name, KEY_TEXT);

    String html = cfg.getString(COMMENTLINK, name, KEY_HTML);
    boolean hasHtml = !Strings.isNullOrEmpty(html);

    String rawEnabled = cfg.getString(COMMENTLINK, name, KEY_ENABLED);
    Boolean enabled;
    if (rawEnabled != null) {
      enabled = cfg.getBoolean(COMMENTLINK, name, KEY_ENABLED, true);
    } else {
      enabled = null;
    }
    checkArgument(allowRaw || !hasHtml, "Raw html replacement not allowed");

    if (Strings.isNullOrEmpty(match)
        && Strings.isNullOrEmpty(link)
        && !hasHtml
        && enabled != null) {
      if (enabled) {
        return StoredCommentLinkInfo.enabled(name);
      }
      return StoredCommentLinkInfo.disabled(name);
    }
    return StoredCommentLinkInfo.builder(name)
        .setMatch(match)
        .setLink(link)
        .setPrefix(linkPrefix)
        .setSuffix(linkSuffix)
        .setText(linkText)
        .setHtml(html)
        .setEnabled(enabled)
        .setOverrideOnly(false)
        .build();
  }

  public void addCommentLinkSection(StoredCommentLinkInfo commentLink) {
    commentLinkSections.put(commentLink.getName(), commentLink);
  }

  public void removeCommentLinkSection(String name) {
    requireNonNull(name);
    requireNonNull(commentLinkSections.remove(name));
  }

  private ProjectConfig(
      Project.NameKey projectName,
      Optional baseConfig,
      AllProjectsName allProjectsName) {
    this.projectName = projectName;
    this.baseConfig = baseConfig;
    this.allProjectsName = allProjectsName;
  }

  public void load(Repository repo) throws IOException, ConfigInvalidException {
    super.load(projectName, repo);
  }

  public void load(Repository repo, @Nullable ObjectId revision)
      throws IOException, ConfigInvalidException {
    super.load(projectName, repo, revision);
  }

  public void load(RevWalk rw, @Nullable ObjectId revision)
      throws IOException, ConfigInvalidException {
    super.load(projectName, rw, revision);
  }

  public Project.NameKey getName() {
    return projectName;
  }

  public Project getProject() {
    return project;
  }

  public void setProject(Project.Builder project) {
    this.project = project.build();
  }

  public void updateProject(Consumer update) {
    Project.Builder builder = project.toBuilder();
    update.accept(builder);
    project = builder.build();
  }

  public AccountsSection getAccountsSection() {
    return accountsSection;
  }

  public void setAccountsSection(AccountsSection accountsSection) {
    this.accountsSection = accountsSection;
  }

  /** Returns an access section, {@code name} typically is a ref pattern. */
  public AccessSection getAccessSection(String name) {
    return accessSections.get(name);
  }

  public void upsertAccessSection(String name, Consumer update) {
    AccessSection.Builder accessSectionBuilder =
        accessSections.containsKey(name)
            ? accessSections.get(name).toBuilder()
            : AccessSection.builder(name);
    update.accept(accessSectionBuilder);
    accessSections.put(name, accessSectionBuilder.build());
  }

  public Collection getAccessSections() {
    return sort(accessSections.values());
  }

  public BranchOrderSection getBranchOrderSection() {
    return branchOrderSection;
  }

  public void setBranchOrderSection(BranchOrderSection branchOrderSection) {
    this.branchOrderSection = branchOrderSection;
  }

  public Map getSubscribeSections() {
    return subscribeSections;
  }

  public void addSubscribeSection(SubscribeSection s) {
    subscribeSections.put(s.project(), s);
  }

  public void remove(AccessSection section) {
    if (section != null) {
      String name = section.getName();
      if (sectionsWithUnknownPermissions.contains(name)) {
        AccessSection.Builder a = accessSections.get(name).toBuilder();
        a.modifyPermissions(List::clear);
        accessSections.put(name, a.build());
      } else {
        accessSections.remove(name);
      }
    }
  }

  public void remove(AccessSection section, Permission permission) {
    if (permission == null) {
      remove(section);
    } else if (section != null) {
      AccessSection a =
          accessSections.get(section.getName()).toBuilder().remove(permission.toBuilder()).build();
      accessSections.put(section.getName(), a);
      if (a.getPermissions().isEmpty()) {
        remove(a);
      }
    }
  }

  public void remove(AccessSection section, Permission permission, PermissionRule rule) {
    if (rule == null) {
      remove(section, permission);
    } else if (section != null && permission != null) {
      AccessSection a = accessSections.get(section.getName());
      if (a == null) {
        return;
      }
      Permission p = a.getPermission(permission.getName());
      if (p == null) {
        return;
      }
      AccessSection.Builder accessSectionBuilder = a.toBuilder();
      Permission.Builder permissionBuilder =
          accessSectionBuilder.upsertPermission(permission.getName());
      permissionBuilder.remove(rule);
      if (permissionBuilder.build().getRules().isEmpty()) {
        accessSectionBuilder.remove(permissionBuilder);
      }
      a = accessSectionBuilder.build();
      accessSections.put(section.getName(), a);
      if (a.getPermissions().isEmpty()) {
        remove(a);
      }
    }
  }

  public ContributorAgreement getContributorAgreement(String name) {
    return contributorAgreements.get(name);
  }

  public Collection getContributorAgreements() {
    return sort(contributorAgreements.values());
  }

  public void replace(ContributorAgreement section) {
    ContributorAgreement.Builder ca = section.toBuilder();
    ca.setAutoVerify(resolve(section.getAutoVerify()));
    ImmutableList.Builder newRules = ImmutableList.builder();
    for (PermissionRule rule : section.getAccepted()) {
      newRules.add(rule.toBuilder().setGroup(resolve(rule.getGroup())).build());
    }
    ca.setAccepted(newRules.build());

    contributorAgreements.put(section.getName(), ca.build());
  }

  public Collection getNotifyConfigs() {
    return notifySections.values();
  }

  public void putNotifyConfig(String name, NotifyConfig nc) {
    notifySections.put(name, nc);
  }

  public Map getLabelSections() {
    return labelSections;
  }

  public Map getSubmitRequirementSections() {
    return submitRequirementSections;
  }

  /** Adds or replaces the given {@link SubmitRequirement} in this config. */
  public void upsertSubmitRequirement(SubmitRequirement requirement) {
    submitRequirementSections.put(requirement.name(), requirement);
  }

  @VisibleForTesting
  public void clearSubmitRequirements() {
    submitRequirementSections = new LinkedHashMap<>();
  }

  /** Adds or replaces the given {@link LabelType} in this config. */
  public void upsertLabelType(LabelType labelType) {
    labelSections.put(labelType.getName(), labelType);
  }

  /** Allows a mutation of an existing {@link LabelType}. */
  public void updateLabelType(String name, Consumer update) {
    LabelType labelType = labelSections.get(name);
    checkState(labelType != null, "labelType must not be null");
    LabelType.Builder builder = labelSections.get(name).toBuilder();
    update.accept(builder);
    upsertLabelType(builder.build());
  }

  /** Adds or replaces the given {@link ContributorAgreement} in this config. */
  public void upsertContributorAgreement(ContributorAgreement ca) {
    contributorAgreements.remove(ca.getName());
    contributorAgreements.put(ca.getName(), ca);
  }

  public Collection getCommentLinkSections() {
    return commentLinkSections.values();
  }

  public ConfiguredMimeTypes getMimeTypes() {
    return mimeTypes;
  }

  public GroupReference resolve(GroupReference group) {
    return groupList.resolve(group);
  }

  public void renameGroup(AccountGroup.UUID uuid, String newName) {
    groupList.renameGroup(uuid, newName);
  }

  /** Returns the group reference, if the group is used by at least one rule. */
  public GroupReference getGroup(AccountGroup.UUID uuid) {
    return groupList.byUUID(uuid);
  }

  /**
   * Returns the group reference corresponding to the specified group name if the group is used by
   * at least one rule or plugin value.
   */
  public GroupReference getGroup(String groupName) {
    return groupList.byName(groupName);
  }

  /**
   * Returns the project's rules.pl ObjectId, if present in the branch. Null if it doesn't exist.
   */
  public ObjectId getRulesId() {
    return rulesId;
  }

  /** Returns the maxObjectSizeLimit configured on this project, or zero if not configured. */
  public long getMaxObjectSizeLimit() {
    return maxObjectSizeLimit;
  }

  /** Returns the checkReceivedObjects for this project, default is true. */
  public boolean getCheckReceivedObjects() {
    return checkReceivedObjects;
  }

  /**
   * Check all GroupReferences use current group name, repairing stale ones.
   *
   * @param groupBackend cache to use when looking up group information by UUID.
   * @return true if one or more group names was stale.
   */
  public boolean updateGroupNames(GroupBackend groupBackend) {
    boolean dirty = false;
    for (GroupReference ref : groupList.references()) {
      GroupDescription.Basic g = groupBackend.get(ref.getUUID());
      if (g != null && !g.getName().equals(ref.getName())) {
        dirty = true;
        groupList.renameGroup(ref.getUUID(), g.getName());
      }
    }
    return dirty;
  }

  /**
   * Get the validation errors, if any were discovered during load.
   *
   * @return list of errors; empty list if there are no errors.
   */
  public List getValidationErrors() {
    if (validationErrors != null) {
      return Collections.unmodifiableList(validationErrors);
    }
    return Collections.emptyList();
  }

  @Override
  protected String getRefName() {
    return RefNames.REFS_CONFIG;
  }

  @Override
  protected void onLoad() throws IOException, ConfigInvalidException {
    if (baseConfig.isPresent()) {
      baseConfig.get().load();
    }
    readGroupList();

    rulesId = getObjectId("rules.pl");
    Config rc = readConfig(PROJECT_CONFIG, baseConfig);
    Project.Builder p = Project.builder(projectName);
    p.setDescription(Strings.nullToEmpty(rc.getString(PROJECT, null, KEY_DESCRIPTION)));
    if (revision != null) {
      p.setConfigRefState(revision.toObjectId().name());
    }

    if (rc.getStringList(ACCESS, null, KEY_INHERIT_FROM).length > 1) {
      // The config must not contain more than one parent to inherit from
      // as there is no guarantee which of the parents would be used then.
      error("Cannot inherit from multiple projects");
    }
    p.setParent(rc.getString(ACCESS, null, KEY_INHERIT_FROM));

    for (BooleanProjectConfig config : BooleanProjectConfig.values()) {
      p.setBooleanConfig(
          config,
          getEnum(
              rc,
              config.getSection(),
              config.getSubSection(),
              config.getName(),
              InheritableBoolean.INHERIT));
    }

    p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));

    p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, DEFAULT_SUBMIT_TYPE));
    p.setState(getEnum(rc, PROJECT, null, KEY_STATE, DEFAULT_STATE_VALUE));

    p.setDefaultDashboard(rc.getString(DASHBOARD, null, KEY_DEFAULT));
    p.setLocalDefaultDashboard(rc.getString(DASHBOARD, null, KEY_LOCAL_DEFAULT));
    this.project = p.build();

    loadAccountsSection(rc);
    loadContributorAgreements(rc);
    loadAccessSections(rc);
    loadBranchOrderSection(rc);
    loadNotifySections(rc);
    loadLabelSections(rc);
    loadSubmitRequirementSections(rc);
    loadCommentLinkSections(rc);
    loadSubscribeSections(rc);
    mimeTypes = ConfiguredMimeTypes.create(projectName.get(), rc);
    loadPluginSections(rc);
    loadProjectLevelConfigs();
    loadReceiveSection(rc);
    loadExtensionPanelSections(rc);
  }

  private void loadAccountsSection(Config rc) {
    accountsSection =
        AccountsSection.create(
            loadPermissionRules(rc, ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY, false));
  }

  private void loadExtensionPanelSections(Config rc) {
    Map lowerNames = Maps.newHashMapWithExpectedSize(2);
    extensionPanelSections = new LinkedHashMap<>();
    for (String name : rc.getSubsections(EXTENSION_PANELS)) {
      String lower = name.toLowerCase();
      if (lowerNames.containsKey(lower)) {
        error(
            String.format(
                "Extension Panels \"%s\" conflicts with \"%s\"", name, lowerNames.get(lower)));
      }
      lowerNames.put(lower, name);
      extensionPanelSections.put(
          name,
          new ArrayList<>(Arrays.asList(rc.getStringList(EXTENSION_PANELS, name, KEY_PANEL))));
    }
  }

  private void loadContributorAgreements(Config rc) {
    contributorAgreements = new HashMap<>();
    for (String name : rc.getSubsections(CONTRIBUTOR_AGREEMENT)) {
      ContributorAgreement.Builder ca = ContributorAgreement.builder(name);
      ca.setDescription(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_DESCRIPTION));
      ca.setAgreementUrl(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_AGREEMENT_URL));
      ca.setAccepted(loadPermissionRules(rc, CONTRIBUTOR_AGREEMENT, name, KEY_ACCEPTED, false));
      ca.setExcludeProjectsRegexes(
          loadPatterns(rc, CONTRIBUTOR_AGREEMENT, name, KEY_EXCLUDE_PROJECTS));
      ca.setMatchProjectsRegexes(loadPatterns(rc, CONTRIBUTOR_AGREEMENT, name, KEY_MATCH_PROJECTS));

      List rules =
          loadPermissionRules(rc, CONTRIBUTOR_AGREEMENT, name, KEY_AUTO_VERIFY, false);
      if (rules.isEmpty()) {
        ca.setAutoVerify(null);
      } else if (rules.size() > 1) {
        error(
            String.format(
                "Invalid rule in %s.%s.%s: at most one group may be set",
                CONTRIBUTOR_AGREEMENT, name, KEY_AUTO_VERIFY));
      } else if (rules.get(0).getAction() != Action.ALLOW) {
        error(
            String.format(
                "Invalid rule in %s.%s.%s: the group must be allowed",
                CONTRIBUTOR_AGREEMENT, name, KEY_AUTO_VERIFY));
      } else {
        ca.setAutoVerify(rules.get(0).getGroup());
      }
      contributorAgreements.put(name, ca.build());
    }
  }

  /**
   * Parses the [notify] sections out of the configuration file.
   *
   * 
   *   [notify "reviewers"]
   *     email = group Reviewers
   *     type = new_changes
   *
   *   [notify "dev-team"]
   *     email = [email protected]
   *     filter = branch:master
   *
   *   [notify "qa"]
   *     email = [email protected]
   *     filter = branch:\"^(maint|stable)-.*\"
   *     type = submitted_changes
   * 
*/ private void loadNotifySections(Config rc) { notifySections = new HashMap<>(); for (String sectionName : rc.getSubsections(NOTIFY)) { NotifyConfig.Builder n = NotifyConfig.builder(); n.setName(sectionName); n.setFilter(rc.getString(NOTIFY, sectionName, KEY_FILTER)); EnumSet types = EnumSet.noneOf(NotifyType.class); types.addAll(ConfigUtil.getEnumList(rc, NOTIFY, sectionName, KEY_TYPE, NotifyType.ALL)); n.setNotify(types); n.setHeader(rc.getEnum(NOTIFY, sectionName, KEY_HEADER, NotifyConfig.Header.BCC)); for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) { String groupName = GroupReference.extractGroupName(dst); if (groupName != null) { GroupReference ref = groupList.byName(groupName); if (ref == null) { ref = groupList.resolve(GroupReference.create(groupName)); } if (ref.getUUID() != null) { n.addGroup(ref); } else { error(String.format("group \"%s\" not in %s", ref.getName(), GroupList.FILE_NAME)); } } else if (dst.startsWith("user ")) { error(String.format("%s not supported", dst)); } else { try { n.addAddress(Address.parse(dst)); } catch (IllegalArgumentException err) { error( String.format("notify section \"%s\" has invalid email \"%s\"", sectionName, dst)); } } } notifySections.put(sectionName, n.build()); } } private void loadAccessSections(Config rc) { accessSections = new HashMap<>(); sectionsWithUnknownPermissions = new HashSet<>(); for (String refName : rc.getSubsections(ACCESS)) { if (AccessSection.isValidRefSectionName(refName) && isValidRegex(refName)) { upsertAccessSection( refName, as -> { for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) { for (String n : Splitter.on(EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN).split(varName)) { n = convertLegacyPermission(n); if (isCoreOrPluginPermission(n)) { as.upsertPermission(n).setExclusiveGroup(true); } } } for (String varName : rc.getNames(ACCESS, refName)) { String convertedName = convertLegacyPermission(varName); if (isCoreOrPluginPermission(convertedName)) { Permission.Builder perm = as.upsertPermission(convertedName); loadPermissionRules( rc, ACCESS, refName, varName, perm, Permission.hasRange(convertedName)); } else { sectionsWithUnknownPermissions.add(as.getName()); } } }); } } AccessSection.Builder capability = null; for (String varName : rc.getNames(CAPABILITY)) { if (capability == null) { capability = AccessSection.builder(AccessSection.GLOBAL_CAPABILITIES); accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability.build()); } Permission.Builder perm = capability.upsertPermission(varName); loadPermissionRules(rc, CAPABILITY, null, varName, perm, GlobalCapability.hasRange(varName)); accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability.build()); } } private boolean isCoreOrPluginPermission(String permission) { // Since plugins are loaded dynamically, here we can't load all plugin permissions and verify // their existence. return isPermission(permission) || isValidPluginPermission(permission); } private boolean isValidRegex(String refPattern) { try { RefPattern.validateRegExp(refPattern); } catch (InvalidNameException e) { error(String.format("Invalid ref name: %s", e.getMessage())); return false; } return true; } private void loadBranchOrderSection(Config rc) { if (rc.getSections().contains(BRANCH_ORDER)) { branchOrderSection = BranchOrderSection.create(Arrays.asList(rc.getStringList(BRANCH_ORDER, null, BRANCH))); } } private void saveBranchOrderSection(Config rc) { if (branchOrderSection != null) { rc.setStringList(BRANCH_ORDER, null, BRANCH, branchOrderSection.order()); } } private ImmutableList loadPatterns( Config rc, String section, String subsection, String varName) { ImmutableList.Builder patterns = ImmutableList.builder(); for (String patternString : rc.getStringList(section, subsection, varName)) { try { // While one could just use getStringList directly, compiling first will cause the server // to fail fast if any of the patterns are invalid. patterns.add(Pattern.compile(patternString).pattern()); } catch (PatternSyntaxException e) { error(String.format("Invalid regular expression: %s", e.getMessage())); continue; } } return patterns.build(); } private ImmutableList loadPermissionRules( Config rc, String section, String subsection, String varName, boolean useRange) { Permission.Builder perm = Permission.builder(varName); loadPermissionRules(rc, section, subsection, varName, perm, useRange); return perm.build().getRules(); } private void loadPermissionRules( Config rc, String section, String subsection, String varName, Permission.Builder perm, boolean useRange) { for (String ruleString : rc.getStringList(section, subsection, varName)) { PermissionRule rule; try { rule = PermissionRule.fromString(ruleString, useRange); } catch (IllegalArgumentException notRule) { error( String.format( "Invalid rule in %s.%s: %s", section + (subsection != null ? "." + subsection : ""), varName, notRule.getMessage())); continue; } GroupReference ref = groupList.byName(rule.getGroup().getName()); if (ref == null) { // The group wasn't mentioned in the groups table, so there is // no valid UUID for it. Pool the reference anyway so at least // all rules in the same file share the same GroupReference. // ref = groupList.resolve(rule.getGroup()); error(String.format("group \"%s\" not in %s", ref.getName(), GroupList.FILE_NAME)); } perm.add(rule.toBuilder().setGroup(ref)); } } private static LabelValue parseLabelValue(String src) { List parts = ImmutableList.copyOf( Splitter.on(CharMatcher.whitespace()).omitEmptyStrings().limit(2).split(src)); if (parts.isEmpty()) { throw new IllegalArgumentException("empty value"); } String valueText = parts.size() > 1 ? parts.get(1) : ""; return LabelValue.create(Shorts.checkedCast(PermissionRule.parseInt(parts.get(0))), valueText); } private void loadSubmitRequirementSections(Config rc) { checkForUnsupportedSubmitRequirementParams(rc); Map lowerNames = new HashMap<>(); submitRequirementSections = new LinkedHashMap<>(); for (String name : rc.getSubsections(SUBMIT_REQUIREMENT)) { String lower = name.toLowerCase(); if (lowerNames.containsKey(lower)) { error( String.format( "Submit requirement '%s' conflicts with '%s'.", name, lowerNames.get(lower))); continue; } lowerNames.put(lower, name); String description = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_DESCRIPTION); String applicabilityExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_APPLICABILITY_EXPRESSION); String submittabilityExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_SUBMITTABILITY_EXPRESSION); String overrideExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_EXPRESSION); boolean canInherit; try { canInherit = rc.getBoolean(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, false); } catch (IllegalArgumentException e) { String canInheritValue = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS); error( String.format( "Invalid value %s.%s.%s for submit requirement '%s': %s", SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, name, canInheritValue)); continue; } if (submittabilityExpr == null) { error( String.format( "Setting a submittability expression for submit requirement '%s' is required:" + " Missing %s.%s.%s", name, SUBMIT_REQUIREMENT, name, KEY_SR_SUBMITTABILITY_EXPRESSION)); continue; } // The expressions are validated in SubmitRequirementConfigValidator. SubmitRequirement submitRequirement = SubmitRequirement.builder() .setName(name) .setDescription(Optional.ofNullable(description)) .setApplicabilityExpression(SubmitRequirementExpression.of(applicabilityExpr)) .setSubmittabilityExpression(SubmitRequirementExpression.create(submittabilityExpr)) .setOverrideExpression(SubmitRequirementExpression.of(overrideExpr)) .setAllowOverrideInChildProjects(canInherit) .build(); submitRequirementSections.put(name, submitRequirement); } } /** * Report unsupported submit requirement parameters as errors. * *

Unsupported are submit requirements parameters that * *

    *
  • are directly set in the {@code submit-requirement} section (as submit requirements are * solely defined in subsections) *
  • are unknown (maybe they were accidentally misspelled?) *
*/ private void checkForUnsupportedSubmitRequirementParams(Config rc) { Set directSubmitRequirementParams = rc.getNames(SUBMIT_REQUIREMENT); if (!directSubmitRequirementParams.isEmpty()) { error( String.format( "Submit requirements must be defined in %s. subsections." + " Setting parameters directly in the %s section is not allowed: %s", SUBMIT_REQUIREMENT, SUBMIT_REQUIREMENT, directSubmitRequirementParams.stream().sorted().collect(toImmutableList()))); } for (String subsection : rc.getSubsections(SUBMIT_REQUIREMENT)) { ImmutableList unknownSubmitRequirementParams = rc.getNames(SUBMIT_REQUIREMENT, subsection).stream() .filter(p -> !SR_KEYS.contains(p)) .collect(toImmutableList()); if (!unknownSubmitRequirementParams.isEmpty()) { error( String.format( "Unsupported parameters for submit requirement '%s': %s", subsection, unknownSubmitRequirementParams)); } } } private void loadLabelSections(Config rc) { Map lowerNames = Maps.newHashMapWithExpectedSize(2); labelSections = new LinkedHashMap<>(); for (String name : rc.getSubsections(LABEL)) { String lower = name.toLowerCase(); if (lowerNames.containsKey(lower)) { error(String.format("Label \"%s\" conflicts with \"%s\"", name, lowerNames.get(lower))); } lowerNames.put(lower, name); List values = new ArrayList<>(); Set allValues = new HashSet<>(); for (String value : rc.getStringList(LABEL, name, KEY_VALUE)) { try { LabelValue labelValue = parseLabelValue(value); if (allValues.add(labelValue.getValue())) { values.add(labelValue); } else { error(String.format("Duplicate %s \"%s\" for label \"%s\"", KEY_VALUE, value, name)); } } catch (IllegalArgumentException notValue) { error( String.format( "Invalid %s \"%s\" for label \"%s\": %s", KEY_VALUE, value, name, notValue.getMessage())); } } LabelType.Builder label; try { label = LabelType.builder(name, values); } catch (IllegalArgumentException badName) { error(String.format("Invalid label \"%s\"", name)); continue; } label.setDescription(Optional.ofNullable(rc.getString(LABEL, name, KEY_LABEL_DESCRIPTION))); String functionName = rc.getString(LABEL, name, KEY_FUNCTION); Optional function = functionName != null ? LabelFunction.parse(functionName) : Optional.of(LabelFunction.MAX_WITH_BLOCK); if (!function.isPresent()) { error( String.format( "Invalid %s for label \"%s\". Valid names are: %s", KEY_FUNCTION, name, Joiner.on(", ").join(LabelFunction.ALL.keySet()))); } label.setFunction(function.orElse(null)); label.setCopyCondition(rc.getString(LABEL, name, KEY_COPY_CONDITION)); if (!values.isEmpty()) { short dv = (short) rc.getInt(LABEL, name, KEY_DEFAULT_VALUE, 0); if (isInRange(dv, values)) { label.setDefaultValue(dv); } else { error(String.format("Invalid %s \"%s\" for label \"%s\"", KEY_DEFAULT_VALUE, dv, name)); } } label.setAllowPostSubmit( rc.getBoolean(LABEL, name, KEY_ALLOW_POST_SUBMIT, LabelType.DEF_ALLOW_POST_SUBMIT)); label.setIgnoreSelfApproval( rc.getBoolean(LABEL, name, KEY_IGNORE_SELF_APPROVAL, LabelType.DEF_IGNORE_SELF_APPROVAL)); label.setCanOverride( rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, LabelType.DEF_CAN_OVERRIDE)); List refPatterns = getStringListOrNull(rc, LABEL, name, KEY_BRANCH); if (refPatterns == null) { label.setRefPatterns(null); } else { for (String pattern : refPatterns) { if (pattern.startsWith("^")) { try { Pattern.compile(pattern); } catch (PatternSyntaxException e) { error( String.format( "Invalid ref pattern \"%s\" in %s.%s.%s: %s", pattern, LABEL, name, KEY_BRANCH, e.getMessage())); } } } label.setRefPatterns(ImmutableList.copyOf(refPatterns)); } labelSections.put(name, label.build()); } } private boolean isInRange(short value, List labelValues) { for (LabelValue lv : labelValues) { if (lv.getValue() == value) { return true; } } return false; } private List getStringListOrNull( Config rc, String section, String subSection, String name) { String[] ac = rc.getStringList(section, subSection, name); return ac.length == 0 ? null : Arrays.asList(ac); } private void loadCommentLinkSections(Config rc) { Set subsections = rc.getSubsections(COMMENTLINK); commentLinkSections = new LinkedHashMap<>(subsections.size()); for (String name : subsections) { try { commentLinkSections.put(name, buildCommentLink(rc, name, false)); } catch (PatternSyntaxException e) { error( String.format( "Invalid pattern \"%s\" in commentlink.%s.match: %s", rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage())); } catch (IllegalArgumentException e) { error( String.format( "Error in pattern \"%s\" in commentlink.%s.match: %s", rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage())); } } } private void loadSubscribeSections(Config rc) throws ConfigInvalidException { Set subsections = rc.getSubsections(SUBSCRIBE_SECTION); subscribeSections = new HashMap<>(); try { for (String projectName : subsections) { Project.NameKey p = Project.nameKey(projectName); SubscribeSection.Builder ss = SubscribeSection.builder(p); for (String s : rc.getStringList(SUBSCRIBE_SECTION, projectName, SUBSCRIBE_MULTI_MATCH_REFS)) { ss.addMultiMatchRefSpec(s); } for (String s : rc.getStringList(SUBSCRIBE_SECTION, projectName, SUBSCRIBE_MATCH_REFS)) { ss.addMatchingRefSpec(s); } subscribeSections.put(p, ss.build()); } } catch (IllegalArgumentException e) { throw new ConfigInvalidException(e.getMessage()); } } private void loadReceiveSection(Config rc) { checkReceivedObjects = rc.getBoolean(RECEIVE, KEY_CHECK_RECEIVED_OBJECTS, true); maxObjectSizeLimit = rc.getLong(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, 0); } private void loadPluginSections(Config rc) { pluginConfigs = new HashMap<>(); for (String plugin : rc.getSubsections(PLUGIN)) { Config pluginConfig = new Config(); pluginConfigs.put(plugin, pluginConfig); for (String name : rc.getNames(PLUGIN, plugin)) { String value = rc.getString(PLUGIN, plugin, name); String groupName = GroupReference.extractGroupName(value); if (groupName != null) { GroupReference ref = groupList.byName(groupName); if (ref == null) { error(String.format("group \"%s\" not in %s", groupName, GroupList.FILE_NAME)); } rc.setString(PLUGIN, plugin, name, value); } pluginConfig.setStringList( PLUGIN, plugin, name, Arrays.asList(rc.getStringList(PLUGIN, plugin, name))); } } } public void updatePluginConfig( String pluginName, Consumer pluginConfigUpdate) { Config pluginConfig = pluginConfigs.get(pluginName); if (pluginConfig == null) { pluginConfig = new Config(); pluginConfigs.put(pluginName, pluginConfig); } pluginConfigUpdate.accept(new PluginConfig.Update(pluginName, pluginConfig, Optional.of(this))); } public PluginConfig getPluginConfig(String pluginName) { Config pluginConfig = pluginConfigs.getOrDefault(pluginName, new Config()); return PluginConfig.create(pluginName, pluginConfig, getCacheable()); } private void loadProjectLevelConfigs() throws IOException { projectLevelConfigs = new HashMap<>(); if (revision == null) { return; } for (PathInfo pathInfo : getPathInfos(true)) { if (pathInfo.path.endsWith(".config") && !PROJECT_CONFIG.equals(pathInfo.path)) { String cfg = readUTF8(pathInfo.path); Config parsedConfig = new Config(); try { parsedConfig.fromText(cfg); projectLevelConfigs.put(pathInfo.path, parsedConfig); } catch (ConfigInvalidException e) { logger.atWarning().withCause(e).log("Unable to parse config"); } } } } private void readGroupList() throws IOException { groupList = GroupList.parse(projectName, readUTF8(GroupList.FILE_NAME), this); } @Override protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException { if (commit.getMessage() == null || "".equals(commit.getMessage())) { commit.setMessage("Updated project configuration\n"); } Config rc = readConfig(PROJECT_CONFIG); Project p = project; if (p.getDescription() != null && !p.getDescription().isEmpty()) { rc.setString(PROJECT, null, KEY_DESCRIPTION, p.getDescription()); } else { rc.unset(PROJECT, null, KEY_DESCRIPTION); } set(rc, ACCESS, null, KEY_INHERIT_FROM, p.getParentName()); for (BooleanProjectConfig config : BooleanProjectConfig.values()) { set( rc, config.getSection(), config.getSubSection(), config.getName(), p.getBooleanConfig(config), InheritableBoolean.INHERIT); } set( rc, RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, validMaxObjectSizeLimit(p.getMaxObjectSizeLimit())); set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), DEFAULT_SUBMIT_TYPE); set(rc, PROJECT, null, KEY_STATE, p.getState(), DEFAULT_STATE_VALUE); set(rc, DASHBOARD, null, KEY_DEFAULT, p.getDefaultDashboard()); set(rc, DASHBOARD, null, KEY_LOCAL_DEFAULT, p.getLocalDefaultDashboard()); Set keepGroups = new HashSet<>(); saveAccountsSection(rc, keepGroups); saveContributorAgreements(rc, keepGroups); saveAccessSections(rc, keepGroups); saveNotifySections(rc, keepGroups); savePluginSections(rc, keepGroups); groupList.retainUUIDs(keepGroups); saveLabelSections(rc); saveSubmitRequirementSections(rc); saveCommentLinkSections(rc); saveSubscribeSections(rc); saveBranchOrderSection(rc); saveConfig(PROJECT_CONFIG, rc); saveGroupList(); return true; } public static String validMaxObjectSizeLimit(String value) throws ConfigInvalidException { if (value == null) { return null; } value = value.trim(); if (value.isEmpty()) { return null; } Config cfg = new Config(); cfg.fromText("[s]\nn=" + value); try { long s = cfg.getLong("s", "n", 0); if (s < 0) { throw new ConfigInvalidException( String.format( "Negative value '%s' not allowed as %s", value, KEY_MAX_OBJECT_SIZE_LIMIT)); } if (s == 0) { // return null for the default so that it is not persisted return null; } return value; } catch (IllegalArgumentException e) { throw new ConfigInvalidException( String.format("Value '%s' not parseable as a Long", value), e); } } private void saveAccountsSection(Config rc, Set keepGroups) { unsetSection(rc, ACCOUNTS); if (accountsSection != null) { rc.setStringList( ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY, ruleToStringList(accountsSection.getSameGroupVisibility(), keepGroups)); } } private void saveCommentLinkSections(Config rc) { unsetSection(rc, COMMENTLINK); if (commentLinkSections != null) { for (StoredCommentLinkInfo cm : commentLinkSections.values()) { rc.setString(COMMENTLINK, cm.getName(), KEY_MATCH, cm.getMatch()); if (!Strings.isNullOrEmpty(cm.getHtml())) { rc.setString(COMMENTLINK, cm.getName(), KEY_HTML, cm.getHtml()); } if (!Strings.isNullOrEmpty(cm.getLink())) { rc.setString(COMMENTLINK, cm.getName(), KEY_LINK, cm.getLink()); } if (!Strings.isNullOrEmpty(cm.getPrefix())) { rc.setString(COMMENTLINK, cm.getName(), KEY_PREFIX, cm.getPrefix()); } if (!Strings.isNullOrEmpty(cm.getSuffix())) { rc.setString(COMMENTLINK, cm.getName(), KEY_SUFFIX, cm.getSuffix()); } if (!Strings.isNullOrEmpty(cm.getText())) { rc.setString(COMMENTLINK, cm.getName(), KEY_TEXT, cm.getText()); } if (cm.getEnabled() != null && !cm.getEnabled()) { rc.setBoolean(COMMENTLINK, cm.getName(), KEY_ENABLED, cm.getEnabled()); } } } } private void saveContributorAgreements(Config rc, Set keepGroups) { unsetSection(rc, CONTRIBUTOR_AGREEMENT); for (ContributorAgreement ca : sort(contributorAgreements.values())) { set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_DESCRIPTION, ca.getDescription()); set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AGREEMENT_URL, ca.getAgreementUrl()); if (ca.getAutoVerify() != null) { if (ca.getAutoVerify().getUUID() != null) { keepGroups.add(ca.getAutoVerify().getUUID()); } String autoVerify = PermissionRule.create(ca.getAutoVerify()).asString(false); set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AUTO_VERIFY, autoVerify); } else { rc.unset(CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AUTO_VERIFY); } rc.setStringList( CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_ACCEPTED, ruleToStringList(ca.getAccepted(), keepGroups)); rc.setStringList( CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_EXCLUDE_PROJECTS, patternToStringList(ca.getExcludeProjectsRegexes())); rc.setStringList( CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_MATCH_PROJECTS, patternToStringList(ca.getMatchProjectsRegexes())); } } private void saveNotifySections(Config rc, Set keepGroups) { unsetSection(rc, NOTIFY); for (NotifyConfig nc : sort(notifySections.values())) { nc.getGroups().stream() .map(GroupReference::getUUID) .filter(Objects::nonNull) .forEach(keepGroups::add); List email = nc.getGroups().stream() .map(gr -> PermissionRule.create(gr).asString(false)) .sorted() .collect(toList()); // Separate stream operation so that emails list contains 2 sorted sub-lists. nc.getAddresses().stream().map(Address::toString).sorted().forEach(email::add); set(rc, NOTIFY, nc.getName(), KEY_HEADER, nc.getHeader(), NotifyConfig.Header.BCC); if (email.isEmpty()) { rc.unset(NOTIFY, nc.getName(), KEY_EMAIL); } else { rc.setStringList(NOTIFY, nc.getName(), KEY_EMAIL, email); } if (nc.getNotify().equals(Sets.immutableEnumSet(NotifyType.ALL))) { rc.unset(NOTIFY, nc.getName(), KEY_TYPE); } else { List types = new ArrayList<>(4); for (NotifyType t : NotifyType.values()) { if (nc.isNotify(t)) { types.add(t.name().toLowerCase(Locale.US)); } } rc.setStringList(NOTIFY, nc.getName(), KEY_TYPE, types); } set(rc, NOTIFY, nc.getName(), KEY_FILTER, nc.getFilter()); } } private List patternToStringList(List list) { return list; } private List ruleToStringList( List list, Set keepGroups) { List rules = new ArrayList<>(); for (PermissionRule rule : sort(list)) { if (rule.getGroup().getUUID() != null) { keepGroups.add(rule.getGroup().getUUID()); } rules.add(rule.asString(false)); } return rules; } private void saveAccessSections(Config rc, Set keepGroups) { unsetSection(rc, CAPABILITY); AccessSection capability = accessSections.get(AccessSection.GLOBAL_CAPABILITIES); if (capability != null) { Set have = new HashSet<>(); for (Permission permission : sort(capability.getPermissions())) { have.add(permission.getName().toLowerCase()); boolean needRange = GlobalCapability.hasRange(permission.getName()); List rules = new ArrayList<>(); for (PermissionRule rule : sort(permission.getRules())) { GroupReference group = resolve(rule.getGroup()); if (group.getUUID() != null) { keepGroups.add(group.getUUID()); } rules.add(rule.toBuilder().setGroup(group).build().asString(needRange)); } rc.setStringList(CAPABILITY, null, permission.getName(), rules); } for (String varName : rc.getNames(CAPABILITY)) { if (!have.contains(varName.toLowerCase())) { rc.unset(CAPABILITY, null, varName); } } } else { rc.unsetSection(CAPABILITY, null); } for (AccessSection as : sort(accessSections.values())) { String refName = as.getName(); if (AccessSection.GLOBAL_CAPABILITIES.equals(refName)) { continue; } StringBuilder doNotInherit = new StringBuilder(); for (Permission perm : sort(as.getPermissions())) { if (perm.getExclusiveGroup()) { if (0 < doNotInherit.length()) { doNotInherit.append(' '); } doNotInherit.append(perm.getName()); } } if (0 < doNotInherit.length()) { rc.setString(ACCESS, refName, KEY_GROUP_PERMISSIONS, doNotInherit.toString()); } else { rc.unset(ACCESS, refName, KEY_GROUP_PERMISSIONS); } Set have = new HashSet<>(); for (Permission permission : sort(as.getPermissions())) { have.add(permission.getName().toLowerCase()); boolean needRange = Permission.hasRange(permission.getName()); List rules = new ArrayList<>(); for (PermissionRule rule : sort(permission.getRules())) { GroupReference group = resolve(rule.getGroup()); if (group.getUUID() != null) { keepGroups.add(group.getUUID()); } rules.add(rule.toBuilder().setGroup(group).build().asString(needRange)); } rc.setStringList(ACCESS, refName, permission.getName(), rules); } for (String varName : rc.getNames(ACCESS, refName)) { if (isCoreOrPluginPermission(convertLegacyPermission(varName)) && !have.contains(varName.toLowerCase())) { rc.unset(ACCESS, refName, varName); } } } for (String name : rc.getSubsections(ACCESS)) { if (AccessSection.isValidRefSectionName(name) && !accessSections.containsKey(name)) { rc.unsetSection(ACCESS, name); } } } private void saveLabelSections(Config rc) { List existing = new ArrayList<>(rc.getSubsections(LABEL)); if (!new ArrayList<>(labelSections.keySet()).equals(existing)) { // Order of sections changed, remove and rewrite them all. unsetSection(rc, LABEL); } Set toUnset = new HashSet<>(existing); for (Map.Entry e : labelSections.entrySet()) { String name = e.getKey(); LabelType label = e.getValue(); toUnset.remove(name); if (label.getDescription().isPresent() && !label.getDescription().get().isEmpty()) { rc.setString(LABEL, name, KEY_LABEL_DESCRIPTION, label.getDescription().get()); } else { rc.unset(LABEL, name, KEY_LABEL_DESCRIPTION); } rc.setString(LABEL, name, KEY_FUNCTION, label.getFunction().getFunctionName()); rc.setInt(LABEL, name, KEY_DEFAULT_VALUE, label.getDefaultValue()); setBooleanConfigKey( rc, LABEL, name, KEY_ALLOW_POST_SUBMIT, label.isAllowPostSubmit(), LabelType.DEF_ALLOW_POST_SUBMIT); setBooleanConfigKey( rc, LABEL, name, KEY_IGNORE_SELF_APPROVAL, label.isIgnoreSelfApproval(), LabelType.DEF_IGNORE_SELF_APPROVAL); setBooleanConfigKey( rc, LABEL, name, KEY_CAN_OVERRIDE, label.isCanOverride(), LabelType.DEF_CAN_OVERRIDE); List values = new ArrayList<>(label.getValues().size()); for (LabelValue value : label.getValues()) { values.add(value.format().trim()); } rc.setStringList(LABEL, name, KEY_VALUE, values); if (label.getCopyCondition().isPresent()) { rc.setString(LABEL, name, KEY_COPY_CONDITION, label.getCopyCondition().get()); } else { rc.unset(LABEL, name, KEY_COPY_CONDITION); } List refPatterns = label.getRefPatterns(); if (refPatterns != null && !refPatterns.isEmpty()) { rc.setStringList(LABEL, name, KEY_BRANCH, refPatterns); } else { rc.unset(LABEL, name, KEY_BRANCH); } } for (String name : toUnset) { rc.unsetSection(LABEL, name); } } private void saveSubmitRequirementSections(Config rc) { unsetSection(rc, SUBMIT_REQUIREMENT); if (submitRequirementSections != null) { for (Map.Entry entry : submitRequirementSections.entrySet()) { String name = entry.getKey(); SubmitRequirement sr = entry.getValue(); if (sr.description().isPresent()) { rc.setString(SUBMIT_REQUIREMENT, name, KEY_SR_DESCRIPTION, sr.description().get()); } if (sr.applicabilityExpression().isPresent()) { rc.setString( SUBMIT_REQUIREMENT, name, KEY_SR_APPLICABILITY_EXPRESSION, sr.applicabilityExpression().get().expressionString()); } rc.setString( SUBMIT_REQUIREMENT, name, KEY_SR_SUBMITTABILITY_EXPRESSION, sr.submittabilityExpression().expressionString()); if (sr.overrideExpression().isPresent()) { rc.setString( SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_EXPRESSION, sr.overrideExpression().get().expressionString()); } rc.setBoolean( SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, sr.allowOverrideInChildProjects()); } } } private static void setBooleanConfigKey( Config rc, String section, String name, String key, boolean value, boolean defaultValue) { if (value == defaultValue) { rc.unset(section, name, key); } else { rc.setBoolean(section, name, key, value); } } private void savePluginSections(Config rc, Set keepGroups) { unsetSection(rc, PLUGIN); for (Map.Entry e : pluginConfigs.entrySet()) { String plugin = e.getKey(); Config pluginConfig = e.getValue(); for (String name : pluginConfig.getNames(PLUGIN, plugin)) { String value = pluginConfig.getString(PLUGIN, plugin, name); String groupName = GroupReference.extractGroupName(value); if (groupName != null) { GroupReference ref = groupList.byName(groupName); if (ref != null && ref.getUUID() != null) { keepGroups.add(ref.getUUID()); pluginConfig.setString(PLUGIN, plugin, name, "group " + ref.getName()); } } rc.setStringList( PLUGIN, plugin, name, Arrays.asList(pluginConfig.getStringList(PLUGIN, plugin, name))); } } } private void saveGroupList() throws IOException { saveUTF8(GroupList.FILE_NAME, groupList.asText()); } private void saveSubscribeSections(Config rc) { for (Project.NameKey p : subscribeSections.keySet()) { SubscribeSection s = subscribeSections.get(p); List matchings = new ArrayList<>(); for (String r : s.matchingRefSpecsAsString()) { matchings.add(r); } rc.setStringList(SUBSCRIBE_SECTION, p.get(), SUBSCRIBE_MATCH_REFS, matchings); List multimatchs = new ArrayList<>(); for (String r : s.multiMatchRefSpecsAsString()) { multimatchs.add(r); } rc.setStringList(SUBSCRIBE_SECTION, p.get(), SUBSCRIBE_MULTI_MATCH_REFS, multimatchs); } } private void unsetSection(Config rc, String sectionName) { for (String subSectionName : rc.getSubsections(sectionName)) { rc.unsetSection(sectionName, subSectionName); } rc.unsetSection(sectionName, null); } private > E getEnum( Config rc, String section, String subsection, String name, E defaultValue) { try { return rc.getEnum(section, subsection, name, defaultValue); } catch (IllegalArgumentException err) { error(err.getMessage()); return defaultValue; } } private void error(String errorMessage) { error(ValidationError.create(PROJECT_CONFIG, errorMessage)); } @Override public void error(ValidationError error) { if (validationErrors == null) { validationErrors = new ArrayList<>(4); } validationErrors.add(error); } private static > ImmutableList sort(Collection m) { return m.stream().sorted().collect(toImmutableList()); } @UsedAt(UsedAt.Project.GOOGLE) public boolean hasLegacyPermissions() { return hasLegacyPermissions; } private String convertLegacyPermission(String permissionName) { switch (permissionName) { case LEGACY_PERMISSION_PUSH_TAG: hasLegacyPermissions = true; return Permission.CREATE_TAG; case LEGACY_PERMISSION_PUSH_SIGNED_TAG: hasLegacyPermissions = true; return Permission.CREATE_SIGNED_TAG; default: return permissionName; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy