summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/project/ProjectConfig.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/project/ProjectConfig.java')
-rw-r--r--java/com/google/gerrit/server/project/ProjectConfig.java1483
1 files changed, 1483 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
new file mode 100644
index 0000000000..81cc78b4e5
--- /dev/null
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -0,0 +1,1483 @@
+// 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.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.common.data.Permission.isPermission;
+import static com.google.gerrit.reviewdb.client.Project.DEFAULT_SUBMIT_TYPE;
+import static java.util.stream.Collectors.toList;
+
+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.Maps;
+import com.google.common.primitives.Shorts;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.ProjectWatches.NotifyType;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.git.BranchOrderSection;
+import com.google.gerrit.server.git.NotifyConfig;
+import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.git.meta.VersionedMetaData;
+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.Map.Entry;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+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.revwalk.RevWalk;
+import org.eclipse.jgit.transport.RefSpec;
+
+public class ProjectConfig extends VersionedMetaData implements ValidationError.Sink {
+ public static final String COMMENTLINK = "commentlink";
+ public static final String LABEL = "label";
+ public static final String KEY_FUNCTION = "function";
+ public static final String KEY_DEFAULT_VALUE = "defaultValue";
+ public static final String KEY_COPY_MIN_SCORE = "copyMinScore";
+ public static final String KEY_ALLOW_POST_SUBMIT = "allowPostSubmit";
+ public static final String KEY_IGNORE_SELF_APPROVAL = "ignoreSelfApproval";
+ public static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
+ public static final String KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE =
+ "copyAllScoresOnMergeFirstParentUpdate";
+ public static final String KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = "copyAllScoresOnTrivialRebase";
+ public static final String KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = "copyAllScoresIfNoCodeChange";
+ public static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoChange";
+ public static final String KEY_VALUE = "value";
+ public static final String KEY_CAN_OVERRIDE = "canOverride";
+ public static final String KEY_BRANCH = "branch";
+
+ private static final String KEY_MATCH = "match";
+ private static final String KEY_HTML = "html";
+ private static final String KEY_LINK = "link";
+ private 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 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,}");
+
+ private Project project;
+ private AccountsSection accountsSection;
+ private GroupList groupList;
+ private Map<String, AccessSection> accessSections;
+ private BranchOrderSection branchOrderSection;
+ private Map<String, ContributorAgreement> contributorAgreements;
+ private Map<String, NotifyConfig> notifySections;
+ private Map<String, LabelType> labelSections;
+ private ConfiguredMimeTypes mimeTypes;
+ private Map<Project.NameKey, SubscribeSection> subscribeSections;
+ private List<CommentLinkInfoImpl> commentLinkSections;
+ private List<ValidationError> validationErrors;
+ private ObjectId rulesId;
+ private long maxObjectSizeLimit;
+ private Map<String, Config> pluginConfigs;
+ private boolean checkReceivedObjects;
+ private Set<String> sectionsWithUnknownPermissions;
+ private boolean hasLegacyPermissions;
+ private Map<String, List<String>> extensionPanelSections;
+ private Map<String, GroupReference> groupsByName;
+
+ public static ProjectConfig read(MetaDataUpdate update)
+ throws IOException, ConfigInvalidException {
+ ProjectConfig r = new ProjectConfig(update.getProjectName());
+ r.load(update);
+ return r;
+ }
+
+ public static ProjectConfig read(MetaDataUpdate update, ObjectId id)
+ throws IOException, ConfigInvalidException {
+ ProjectConfig r = new ProjectConfig(update.getProjectName());
+ r.load(update, id);
+ return r;
+ }
+
+ public static CommentLinkInfoImpl 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 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 new CommentLinkInfoImpl.Enabled(name);
+ }
+ return new CommentLinkInfoImpl.Disabled(name);
+ }
+ return new CommentLinkInfoImpl(name, match, link, html, enabled);
+ }
+
+ public void addCommentLinkSection(CommentLinkInfoImpl commentLink) {
+ commentLinkSections.add(commentLink);
+ }
+
+ public ProjectConfig(Project.NameKey projectName) {
+ this.projectName = projectName;
+ }
+
+ 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 AccountsSection getAccountsSection() {
+ return accountsSection;
+ }
+
+ public Map<String, List<String>> getExtensionPanelSections() {
+ return extensionPanelSections;
+ }
+
+ public AccessSection getAccessSection(String name) {
+ return getAccessSection(name, false);
+ }
+
+ public AccessSection getAccessSection(String name, boolean create) {
+ AccessSection as = accessSections.get(name);
+ if (as == null && create) {
+ as = new AccessSection(name);
+ accessSections.put(name, as);
+ }
+ return as;
+ }
+
+ public Collection<AccessSection> getAccessSections() {
+ return sort(accessSections.values());
+ }
+
+ public BranchOrderSection getBranchOrderSection() {
+ return branchOrderSection;
+ }
+
+ public Map<Project.NameKey, SubscribeSection> getSubscribeSections() {
+ return subscribeSections;
+ }
+
+ public Collection<SubscribeSection> getSubscribeSections(Branch.NameKey branch) {
+ Collection<SubscribeSection> ret = new ArrayList<>();
+ for (SubscribeSection s : subscribeSections.values()) {
+ if (s.appliesTo(branch)) {
+ ret.add(s);
+ }
+ }
+ return ret;
+ }
+
+ public void addSubscribeSection(SubscribeSection s) {
+ subscribeSections.put(s.getProject(), s);
+ }
+
+ public void remove(AccessSection section) {
+ if (section != null) {
+ String name = section.getName();
+ if (sectionsWithUnknownPermissions.contains(name)) {
+ AccessSection a = accessSections.get(name);
+ a.setPermissions(new ArrayList<Permission>());
+ } 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());
+ a.remove(permission);
+ 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;
+ }
+ p.remove(rule);
+ if (p.getRules().isEmpty()) {
+ a.remove(permission);
+ }
+ if (a.getPermissions().isEmpty()) {
+ remove(a);
+ }
+ }
+ }
+
+ public void replace(AccessSection section) {
+ for (Permission permission : section.getPermissions()) {
+ for (PermissionRule rule : permission.getRules()) {
+ rule.setGroup(resolve(rule.getGroup()));
+ }
+ }
+
+ accessSections.put(section.getName(), section);
+ }
+
+ public ContributorAgreement getContributorAgreement(String name) {
+ return getContributorAgreement(name, false);
+ }
+
+ public ContributorAgreement getContributorAgreement(String name, boolean create) {
+ ContributorAgreement ca = contributorAgreements.get(name);
+ if (ca == null && create) {
+ ca = new ContributorAgreement(name);
+ contributorAgreements.put(name, ca);
+ }
+ return ca;
+ }
+
+ public Collection<ContributorAgreement> getContributorAgreements() {
+ return sort(contributorAgreements.values());
+ }
+
+ public void remove(ContributorAgreement section) {
+ if (section != null) {
+ accessSections.remove(section.getName());
+ }
+ }
+
+ public void replace(ContributorAgreement section) {
+ section.setAutoVerify(resolve(section.getAutoVerify()));
+ for (PermissionRule rule : section.getAccepted()) {
+ rule.setGroup(resolve(rule.getGroup()));
+ }
+
+ contributorAgreements.put(section.getName(), section);
+ }
+
+ public Collection<NotifyConfig> getNotifyConfigs() {
+ return notifySections.values();
+ }
+
+ public void putNotifyConfig(String name, NotifyConfig nc) {
+ notifySections.put(name, nc);
+ }
+
+ public Map<String, LabelType> getLabelSections() {
+ return labelSections;
+ }
+
+ public Collection<CommentLinkInfoImpl> getCommentLinkSections() {
+ return commentLinkSections;
+ }
+
+ public ConfiguredMimeTypes getMimeTypes() {
+ return mimeTypes;
+ }
+
+ public GroupReference resolve(GroupReference group) {
+ GroupReference groupRef = groupList.resolve(group);
+ if (groupRef != null
+ && groupRef.getUUID() != null
+ && !groupsByName.containsKey(groupRef.getName())) {
+ groupsByName.put(groupRef.getName(), groupRef);
+ }
+ return groupRef;
+ }
+
+ /** @return the group reference, if the group is used by at least one rule. */
+ public GroupReference getGroup(AccountGroup.UUID uuid) {
+ return groupList.byUUID(uuid);
+ }
+
+ /**
+ * @return 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 groupsByName.get(groupName);
+ }
+
+ /** @return set of all groups used by this configuration. */
+ public Set<AccountGroup.UUID> getAllGroupUUIDs() {
+ return groupList.uuids();
+ }
+
+ /**
+ * @return the project's rules.pl ObjectId, if present in the branch. Null if it doesn't exist.
+ */
+ public ObjectId getRulesId() {
+ return rulesId;
+ }
+
+ /** @return the maxObjectSizeLimit configured on this project, or zero if not configured. */
+ public long getMaxObjectSizeLimit() {
+ return maxObjectSizeLimit;
+ }
+
+ /** @return 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;
+ ref.setName(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<ValidationError> 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 {
+ readGroupList();
+ groupsByName = mapGroupReferences();
+
+ rulesId = getObjectId("rules.pl");
+ Config rc = readConfig(PROJECT_CONFIG);
+ project = new Project(projectName);
+
+ Project p = project;
+ p.setDescription(rc.getString(PROJECT, null, KEY_DESCRIPTION));
+ if (p.getDescription() == null) {
+ p.setDescription("");
+ }
+ 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(new ValidationError(PROJECT_CONFIG, "Cannot inherit from multiple projects"));
+ }
+ p.setParentName(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));
+
+ loadAccountsSection(rc);
+ loadContributorAgreements(rc);
+ loadAccessSections(rc);
+ loadBranchOrderSection(rc);
+ loadNotifySections(rc);
+ loadLabelSections(rc);
+ loadCommentLinkSections(rc);
+ loadSubscribeSections(rc);
+ mimeTypes = new ConfiguredMimeTypes(projectName.get(), rc);
+ loadPluginSections(rc);
+ loadReceiveSection(rc);
+ loadExtensionPanelSections(rc);
+ }
+
+ private void loadAccountsSection(Config rc) {
+ accountsSection = new AccountsSection();
+ accountsSection.setSameGroupVisibility(
+ loadPermissionRules(rc, ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY, groupsByName, false));
+ }
+
+ private void loadExtensionPanelSections(Config rc) {
+ Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
+ extensionPanelSections = new LinkedHashMap<>();
+ for (String name : rc.getSubsections(EXTENSION_PANELS)) {
+ String lower = name.toLowerCase();
+ if (lowerNames.containsKey(lower)) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ 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 ca = getContributorAgreement(name, true);
+ 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, groupsByName, false));
+
+ List<PermissionRule> rules =
+ loadPermissionRules(
+ rc, CONTRIBUTOR_AGREEMENT, name, KEY_AUTO_VERIFY, groupsByName, false);
+ if (rules.isEmpty()) {
+ ca.setAutoVerify(null);
+ } else if (rules.size() > 1) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ "Invalid rule in "
+ + CONTRIBUTOR_AGREEMENT
+ + "."
+ + name
+ + "."
+ + KEY_AUTO_VERIFY
+ + ": at most one group may be set"));
+ } else if (rules.get(0).getAction() != Action.ALLOW) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ "Invalid rule in "
+ + CONTRIBUTOR_AGREEMENT
+ + "."
+ + name
+ + "."
+ + KEY_AUTO_VERIFY
+ + ": the group must be allowed"));
+ } else {
+ ca.setAutoVerify(rules.get(0).getGroup());
+ }
+ }
+ }
+
+ /**
+ * Parses the [notify] sections out of the configuration file.
+ *
+ * <pre>
+ * [notify "reviewers"]
+ * email = group Reviewers
+ * type = new_changes
+ *
+ * [notify "dev-team"]
+ * email = dev-team@example.com
+ * filter = branch:master
+ *
+ * [notify "qa"]
+ * email = qa@example.com
+ * filter = branch:\"^(maint|stable)-.*\"
+ * type = submitted_changes
+ * </pre>
+ */
+ private void loadNotifySections(Config rc) {
+ notifySections = new HashMap<>();
+ for (String sectionName : rc.getSubsections(NOTIFY)) {
+ NotifyConfig n = new NotifyConfig();
+ n.setName(sectionName);
+ n.setFilter(rc.getString(NOTIFY, sectionName, KEY_FILTER));
+
+ EnumSet<NotifyType> types = EnumSet.noneOf(NotifyType.class);
+ types.addAll(ConfigUtil.getEnumList(rc, NOTIFY, sectionName, KEY_TYPE, NotifyType.ALL));
+ n.setTypes(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 = groupsByName.get(groupName);
+ if (ref == null) {
+ ref = new GroupReference(null, groupName);
+ groupsByName.put(ref.getName(), ref);
+ }
+ if (ref.getUUID() != null) {
+ n.addEmail(ref);
+ } else {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
+ }
+ } else if (dst.startsWith("user ")) {
+ error(new ValidationError(PROJECT_CONFIG, dst + " not supported"));
+ } else {
+ try {
+ n.addEmail(Address.parse(dst));
+ } catch (IllegalArgumentException err) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ "notify section \"" + sectionName + "\" has invalid email \"" + dst + "\""));
+ }
+ }
+ }
+ notifySections.put(sectionName, n);
+ }
+ }
+
+ private void loadAccessSections(Config rc) {
+ accessSections = new HashMap<>();
+ sectionsWithUnknownPermissions = new HashSet<>();
+ for (String refName : rc.getSubsections(ACCESS)) {
+ if (RefConfigSection.isValid(refName) && isValidRegex(refName)) {
+ AccessSection as = getAccessSection(refName, true);
+
+ 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 (isPermission(n)) {
+ as.getPermission(n, true).setExclusiveGroup(true);
+ }
+ }
+ }
+
+ for (String varName : rc.getNames(ACCESS, refName)) {
+ String convertedName = convertLegacyPermission(varName);
+ if (isPermission(convertedName)) {
+ Permission perm = as.getPermission(convertedName, true);
+ loadPermissionRules(
+ rc,
+ ACCESS,
+ refName,
+ varName,
+ groupsByName,
+ perm,
+ Permission.hasRange(convertedName));
+ } else {
+ sectionsWithUnknownPermissions.add(as.getName());
+ }
+ }
+ }
+ }
+
+ AccessSection capability = null;
+ for (String varName : rc.getNames(CAPABILITY)) {
+ if (capability == null) {
+ capability = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
+ accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability);
+ }
+ Permission perm = capability.getPermission(varName, true);
+ loadPermissionRules(
+ rc, CAPABILITY, null, varName, groupsByName, perm, GlobalCapability.hasRange(varName));
+ }
+ }
+
+ private boolean isValidRegex(String refPattern) {
+ try {
+ RefPattern.validateRegExp(refPattern);
+ } catch (InvalidNameException e) {
+ error(new ValidationError(PROJECT_CONFIG, "Invalid ref name: " + e.getMessage()));
+ return false;
+ }
+ return true;
+ }
+
+ private void loadBranchOrderSection(Config rc) {
+ if (rc.getSections().contains(BRANCH_ORDER)) {
+ branchOrderSection = new BranchOrderSection(rc.getStringList(BRANCH_ORDER, null, BRANCH));
+ }
+ }
+
+ private ImmutableList<PermissionRule> loadPermissionRules(
+ Config rc,
+ String section,
+ String subsection,
+ String varName,
+ Map<String, GroupReference> groupsByName,
+ boolean useRange) {
+ Permission perm = new Permission(varName);
+ loadPermissionRules(rc, section, subsection, varName, groupsByName, perm, useRange);
+ return ImmutableList.copyOf(perm.getRules());
+ }
+
+ private void loadPermissionRules(
+ Config rc,
+ String section,
+ String subsection,
+ String varName,
+ Map<String, GroupReference> groupsByName,
+ Permission perm,
+ boolean useRange) {
+ for (String ruleString : rc.getStringList(section, subsection, varName)) {
+ PermissionRule rule;
+ try {
+ rule = PermissionRule.fromString(ruleString, useRange);
+ } catch (IllegalArgumentException notRule) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ "Invalid rule in "
+ + section
+ + (subsection != null ? "." + subsection : "")
+ + "."
+ + varName
+ + ": "
+ + notRule.getMessage()));
+ continue;
+ }
+
+ GroupReference ref = groupsByName.get(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 = rule.getGroup();
+ groupsByName.put(ref.getName(), ref);
+ error(
+ new ValidationError(
+ PROJECT_CONFIG, "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
+ }
+
+ rule.setGroup(ref);
+ perm.add(rule);
+ }
+ }
+
+ private static LabelValue parseLabelValue(String src) {
+ List<String> 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 new LabelValue(Shorts.checkedCast(PermissionRule.parseInt(parts.get(0))), valueText);
+ }
+
+ private void loadLabelSections(Config rc) {
+ Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
+ labelSections = new LinkedHashMap<>();
+ for (String name : rc.getSubsections(LABEL)) {
+ String lower = name.toLowerCase();
+ if (lowerNames.containsKey(lower)) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ String.format("Label \"%s\" conflicts with \"%s\"", name, lowerNames.get(lower))));
+ }
+ lowerNames.put(lower, name);
+
+ List<LabelValue> values = new ArrayList<>();
+ for (String value : rc.getStringList(LABEL, name, KEY_VALUE)) {
+ try {
+ values.add(parseLabelValue(value));
+ } catch (IllegalArgumentException notValue) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ String.format(
+ "Invalid %s \"%s\" for label \"%s\": %s",
+ KEY_VALUE, value, name, notValue.getMessage())));
+ }
+ }
+
+ LabelType label;
+ try {
+ label = new LabelType(name, values);
+ } catch (IllegalArgumentException badName) {
+ error(new ValidationError(PROJECT_CONFIG, String.format("Invalid label \"%s\"", name)));
+ continue;
+ }
+
+ String functionName = rc.getString(LABEL, name, KEY_FUNCTION);
+ Optional<LabelFunction> function =
+ functionName != null
+ ? LabelFunction.parse(functionName)
+ : Optional.of(LabelFunction.MAX_WITH_BLOCK);
+ if (!function.isPresent()) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ 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));
+
+ if (!values.isEmpty()) {
+ short dv = (short) rc.getInt(LABEL, name, KEY_DEFAULT_VALUE, 0);
+ if (isInRange(dv, values)) {
+ label.setDefaultValue(dv);
+ } else {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ 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.setCopyMinScore(
+ rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, LabelType.DEF_COPY_MIN_SCORE));
+ label.setCopyMaxScore(
+ rc.getBoolean(LABEL, name, KEY_COPY_MAX_SCORE, LabelType.DEF_COPY_MAX_SCORE));
+ label.setCopyAllScoresOnMergeFirstParentUpdate(
+ rc.getBoolean(
+ LABEL,
+ name,
+ KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE,
+ LabelType.DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE));
+ label.setCopyAllScoresOnTrivialRebase(
+ rc.getBoolean(
+ LABEL,
+ name,
+ KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE,
+ LabelType.DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE));
+ label.setCopyAllScoresIfNoCodeChange(
+ rc.getBoolean(
+ LABEL,
+ name,
+ KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE,
+ LabelType.DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE));
+ label.setCopyAllScoresIfNoChange(
+ rc.getBoolean(
+ LABEL,
+ name,
+ KEY_COPY_ALL_SCORES_IF_NO_CHANGE,
+ LabelType.DEF_COPY_ALL_SCORES_IF_NO_CHANGE));
+ label.setCanOverride(
+ rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, LabelType.DEF_CAN_OVERRIDE));
+ label.setRefPatterns(getStringListOrNull(rc, LABEL, name, KEY_BRANCH));
+ labelSections.put(name, label);
+ }
+ }
+
+ private boolean isInRange(short value, List<LabelValue> labelValues) {
+ for (LabelValue lv : labelValues) {
+ if (lv.getValue() == value) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private List<String> 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<String> subsections = rc.getSubsections(COMMENTLINK);
+ commentLinkSections = new ArrayList<>(subsections.size());
+ for (String name : subsections) {
+ try {
+ commentLinkSections.add(buildCommentLink(rc, name, false));
+ } catch (PatternSyntaxException e) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ String.format(
+ "Invalid pattern \"%s\" in commentlink.%s.match: %s",
+ rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage())));
+ } catch (IllegalArgumentException e) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ 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<String> subsections = rc.getSubsections(SUBSCRIBE_SECTION);
+ subscribeSections = new HashMap<>();
+ try {
+ for (String projectName : subsections) {
+ Project.NameKey p = new Project.NameKey(projectName);
+ SubscribeSection ss = new SubscribeSection(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);
+ }
+ } 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 = groupsByName.get(groupName);
+ if (ref == null) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG, "group \"" + groupName + "\" not in " + GroupList.FILE_NAME));
+ }
+ rc.setString(PLUGIN, plugin, name, value);
+ }
+ pluginConfig.setStringList(
+ PLUGIN, plugin, name, Arrays.asList(rc.getStringList(PLUGIN, plugin, name)));
+ }
+ }
+ }
+
+ public PluginConfig getPluginConfig(String pluginName) {
+ Config pluginConfig = pluginConfigs.get(pluginName);
+ if (pluginConfig == null) {
+ pluginConfig = new Config();
+ pluginConfigs.put(pluginName, pluginConfig);
+ }
+ return new PluginConfig(pluginName, pluginConfig, this);
+ }
+
+ private void readGroupList() throws IOException {
+ groupList = GroupList.parse(projectName, readUTF8(GroupList.FILE_NAME), this);
+ }
+
+ private Map<String, GroupReference> mapGroupReferences() {
+ Collection<GroupReference> references = groupList.references();
+ Map<String, GroupReference> result = new HashMap<>(references.size());
+ for (GroupReference ref : references) {
+ result.put(ref.getName(), ref);
+ }
+
+ return result;
+ }
+
+ @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.getConfiguredSubmitType(), 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<AccountGroup.UUID> keepGroups = new HashSet<>();
+ saveAccountsSection(rc, keepGroups);
+ saveContributorAgreements(rc, keepGroups);
+ saveAccessSections(rc, keepGroups);
+ saveNotifySections(rc, keepGroups);
+ savePluginSections(rc, keepGroups);
+ groupList.retainUUIDs(keepGroups);
+ saveLabelSections(rc);
+ saveCommentLinkSections(rc);
+ saveSubscribeSections(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<AccountGroup.UUID> keepGroups) {
+ if (accountsSection != null) {
+ rc.setStringList(
+ ACCOUNTS,
+ null,
+ KEY_SAME_GROUP_VISIBILITY,
+ ruleToStringList(accountsSection.getSameGroupVisibility(), keepGroups));
+ }
+ }
+
+ private void saveCommentLinkSections(Config rc) {
+ if (commentLinkSections != null) {
+ for (CommentLinkInfoImpl cm : commentLinkSections) {
+ rc.setString(COMMENTLINK, cm.name, KEY_MATCH, cm.match);
+ if (!Strings.isNullOrEmpty(cm.html)) {
+ rc.setString(COMMENTLINK, cm.name, KEY_HTML, cm.html);
+ }
+ if (!Strings.isNullOrEmpty(cm.link)) {
+ rc.setString(COMMENTLINK, cm.name, KEY_LINK, cm.link);
+ }
+ if (cm.enabled != null && !cm.enabled) {
+ rc.setBoolean(COMMENTLINK, cm.name, KEY_ENABLED, cm.enabled);
+ }
+ }
+ }
+ }
+
+ private void saveContributorAgreements(Config rc, Set<AccountGroup.UUID> keepGroups) {
+ 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 = new PermissionRule(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));
+ }
+ }
+
+ private void saveNotifySections(Config rc, Set<AccountGroup.UUID> keepGroups) {
+ for (NotifyConfig nc : sort(notifySections.values())) {
+ nc.getGroups().stream()
+ .map(gr -> gr.getUUID())
+ .filter(Objects::nonNull)
+ .forEach(keepGroups::add);
+ List<String> email =
+ nc.getGroups().stream()
+ .map(gr -> new PermissionRule(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(EnumSet.of(NotifyType.ALL))) {
+ rc.unset(NOTIFY, nc.getName(), KEY_TYPE);
+ } else {
+ List<String> 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<String> ruleToStringList(
+ List<PermissionRule> list, Set<AccountGroup.UUID> keepGroups) {
+ List<String> 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<AccountGroup.UUID> keepGroups) {
+ AccessSection capability = accessSections.get(AccessSection.GLOBAL_CAPABILITIES);
+ if (capability != null) {
+ Set<String> have = new HashSet<>();
+ for (Permission permission : sort(capability.getPermissions())) {
+ have.add(permission.getName().toLowerCase());
+
+ boolean needRange = GlobalCapability.hasRange(permission.getName());
+ List<String> 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.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<String> have = new HashSet<>();
+ for (Permission permission : sort(as.getPermissions())) {
+ have.add(permission.getName().toLowerCase());
+
+ boolean needRange = Permission.hasRange(permission.getName());
+ List<String> 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.asString(needRange));
+ }
+ rc.setStringList(ACCESS, refName, permission.getName(), rules);
+ }
+
+ for (String varName : rc.getNames(ACCESS, refName)) {
+ if (isPermission(convertLegacyPermission(varName))
+ && !have.contains(varName.toLowerCase())) {
+ rc.unset(ACCESS, refName, varName);
+ }
+ }
+ }
+
+ for (String name : rc.getSubsections(ACCESS)) {
+ if (RefConfigSection.isValid(name) && !accessSections.containsKey(name)) {
+ rc.unsetSection(ACCESS, name);
+ }
+ }
+ }
+
+ private void saveLabelSections(Config rc) {
+ List<String> existing = new ArrayList<>(rc.getSubsections(LABEL));
+ if (!new ArrayList<>(labelSections.keySet()).equals(existing)) {
+ // Order of sections changed, remove and rewrite them all.
+ for (String name : existing) {
+ rc.unsetSection(LABEL, name);
+ }
+ }
+
+ Set<String> toUnset = new HashSet<>(existing);
+ for (Map.Entry<String, LabelType> e : labelSections.entrySet()) {
+ String name = e.getKey();
+ LabelType label = e.getValue();
+ toUnset.remove(name);
+ 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.allowPostSubmit(),
+ LabelType.DEF_ALLOW_POST_SUBMIT);
+ setBooleanConfigKey(
+ rc,
+ LABEL,
+ name,
+ KEY_IGNORE_SELF_APPROVAL,
+ label.ignoreSelfApproval(),
+ LabelType.DEF_IGNORE_SELF_APPROVAL);
+ setBooleanConfigKey(
+ rc,
+ LABEL,
+ name,
+ KEY_COPY_MIN_SCORE,
+ label.isCopyMinScore(),
+ LabelType.DEF_COPY_MIN_SCORE);
+ setBooleanConfigKey(
+ rc,
+ LABEL,
+ name,
+ KEY_COPY_MAX_SCORE,
+ label.isCopyMaxScore(),
+ LabelType.DEF_COPY_MAX_SCORE);
+ setBooleanConfigKey(
+ rc,
+ LABEL,
+ name,
+ KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE,
+ label.isCopyAllScoresOnTrivialRebase(),
+ LabelType.DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE);
+ setBooleanConfigKey(
+ rc,
+ LABEL,
+ name,
+ KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE,
+ label.isCopyAllScoresIfNoCodeChange(),
+ LabelType.DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE);
+ setBooleanConfigKey(
+ rc,
+ LABEL,
+ name,
+ KEY_COPY_ALL_SCORES_IF_NO_CHANGE,
+ label.isCopyAllScoresIfNoChange(),
+ LabelType.DEF_COPY_ALL_SCORES_IF_NO_CHANGE);
+ setBooleanConfigKey(
+ rc,
+ LABEL,
+ name,
+ KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE,
+ label.isCopyAllScoresOnMergeFirstParentUpdate(),
+ LabelType.DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE);
+ setBooleanConfigKey(
+ rc, LABEL, name, KEY_CAN_OVERRIDE, label.canOverride(), LabelType.DEF_CAN_OVERRIDE);
+ List<String> values = new ArrayList<>(label.getValues().size());
+ for (LabelValue value : label.getValues()) {
+ values.add(value.format().trim());
+ }
+ rc.setStringList(LABEL, name, KEY_VALUE, values);
+
+ List<String> refPatterns = label.getRefPatterns();
+ if (refPatterns != null && !refPatterns.isEmpty()) {
+ rc.setStringList(LABEL, name, KEY_BRANCH, refPatterns);
+ }
+ }
+
+ for (String name : toUnset) {
+ rc.unsetSection(LABEL, name);
+ }
+ }
+
+ 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<AccountGroup.UUID> keepGroups) {
+ List<String> existing = new ArrayList<>(rc.getSubsections(PLUGIN));
+ for (String name : existing) {
+ rc.unsetSection(PLUGIN, name);
+ }
+
+ for (Entry<String, Config> 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 = groupsByName.get(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<String> matchings = new ArrayList<>();
+ for (RefSpec r : s.getMatchingRefSpecs()) {
+ matchings.add(r.toString());
+ }
+ rc.setStringList(SUBSCRIBE_SECTION, p.get(), SUBSCRIBE_MATCH_REFS, matchings);
+
+ List<String> multimatchs = new ArrayList<>();
+ for (RefSpec r : s.getMultiMatchRefSpecs()) {
+ multimatchs.add(r.toString());
+ }
+ rc.setStringList(SUBSCRIBE_SECTION, p.get(), SUBSCRIBE_MULTI_MATCH_REFS, multimatchs);
+ }
+ }
+
+ private <E extends Enum<?>> E getEnum(
+ Config rc, String section, String subsection, String name, E defaultValue) {
+ try {
+ return rc.getEnum(section, subsection, name, defaultValue);
+ } catch (IllegalArgumentException err) {
+ error(new ValidationError(PROJECT_CONFIG, err.getMessage()));
+ return defaultValue;
+ }
+ }
+
+ @Override
+ public void error(ValidationError error) {
+ if (validationErrors == null) {
+ validationErrors = new ArrayList<>(4);
+ }
+ validationErrors.add(error);
+ }
+
+ private static <T extends Comparable<? super T>> ImmutableList<T> sort(Collection<T> m) {
+ return m.stream().sorted().collect(toImmutableList());
+ }
+
+ 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;
+ }
+ }
+}