diff options
Diffstat (limited to 'java/com/google/gerrit/server/project/ProjectConfig.java')
-rw-r--r-- | java/com/google/gerrit/server/project/ProjectConfig.java | 1483 |
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; + } + } +} |