| // 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 com.google.gerrit.server.permissions.PluginPermissionsUtil.isValidPluginPermission; |
| 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.UsedAt; |
| 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.SubscribeSection; |
| import com.google.gerrit.exceptions.InvalidNameException; |
| import com.google.gerrit.extensions.client.InheritableBoolean; |
| import com.google.gerrit.extensions.client.ProjectState; |
| import com.google.gerrit.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.AllProjectsName; |
| import com.google.gerrit.server.config.ConfigUtil; |
| import com.google.gerrit.server.config.PluginConfig; |
| import com.google.gerrit.server.config.SitePaths; |
| 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 com.google.inject.Inject; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.CommitBuilder; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.lib.StoredConfig; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.storage.file.FileBasedConfig; |
| import org.eclipse.jgit.transport.RefSpec; |
| import org.eclipse.jgit.util.FS; |
| |
| 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 KEY_MATCH_PROJECTS = "matchProjects"; |
| private static final String KEY_EXCLUDE_PROJECTS = "excludeProjects"; |
| |
| private static final String NOTIFY = "notify"; |
| private static final String KEY_EMAIL = "email"; |
| private static final String KEY_FILTER = "filter"; |
| private static final String KEY_TYPE = "type"; |
| private static final String KEY_HEADER = "header"; |
| |
| private static final String CAPABILITY = "capability"; |
| |
| private static final String RECEIVE = "receive"; |
| private static final String KEY_CHECK_RECEIVED_OBJECTS = "checkReceivedObjects"; |
| |
| private static final String SUBMIT = "submit"; |
| private static final String KEY_ACTION = "action"; |
| private static final String KEY_STATE = "state"; |
| |
| private static final String KEY_MAX_OBJECT_SIZE_LIMIT = "maxObjectSizeLimit"; |
| |
| private static final String SUBSCRIBE_SECTION = "allowSuperproject"; |
| private static final String SUBSCRIBE_MATCH_REFS = "matching"; |
| private static final String SUBSCRIBE_MULTI_MATCH_REFS = "all"; |
| |
| private static final String DASHBOARD = "dashboard"; |
| private static final String KEY_DEFAULT = "default"; |
| private static final String KEY_LOCAL_DEFAULT = "local-default"; |
| |
| private static final String LEGACY_PERMISSION_PUSH_TAG = "pushTag"; |
| private static final String LEGACY_PERMISSION_PUSH_SIGNED_TAG = "pushSignedTag"; |
| |
| private static final String PLUGIN = "plugin"; |
| |
| private static final ProjectState DEFAULT_STATE_VALUE = ProjectState.ACTIVE; |
| |
| private static final String EXTENSION_PANELS = "extension-panels"; |
| private static final String KEY_PANEL = "panel"; |
| |
| private static final Pattern EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN = Pattern.compile("[, \t]{1,}"); |
| |
| // Don't use an assisted factory, since instances created by an assisted factory retain references |
| // to their enclosing injector. Instances of ProjectConfig are cached for a long time in the |
| // ProjectCache, so this would retain lots more memory. |
| @Singleton |
| public static class Factory { |
| @Nullable |
| public static StoredConfig getBaseConfig( |
| SitePaths sitePaths, AllProjectsName allProjects, Project.NameKey projectName) { |
| return projectName.equals(allProjects) |
| // Delay loading till onLoad method. |
| ? new FileBasedConfig( |
| sitePaths.etc_dir.resolve(allProjects.get()).resolve(PROJECT_CONFIG).toFile(), |
| FS.DETECTED) |
| : null; |
| } |
| |
| private final SitePaths sitePaths; |
| private final AllProjectsName allProjects; |
| |
| @Inject |
| Factory(SitePaths sitePaths, AllProjectsName allProjects) { |
| this.sitePaths = sitePaths; |
| this.allProjects = allProjects; |
| } |
| |
| public ProjectConfig create(Project.NameKey projectName) { |
| return new ProjectConfig(projectName, getBaseConfig(sitePaths, allProjects, projectName)); |
| } |
| |
| public ProjectConfig read(MetaDataUpdate update) throws IOException, ConfigInvalidException { |
| ProjectConfig r = create(update.getProjectName()); |
| r.load(update); |
| return r; |
| } |
| |
| public ProjectConfig read(MetaDataUpdate update, ObjectId id) |
| throws IOException, ConfigInvalidException { |
| ProjectConfig r = create(update.getProjectName()); |
| r.load(update, id); |
| return r; |
| } |
| |
| @UsedAt(UsedAt.Project.COLLABNET) |
| public ProjectConfig read(Repository repo, Project.NameKey name) |
| throws IOException, ConfigInvalidException { |
| ProjectConfig r = create(name); |
| r.load(repo); |
| return r; |
| } |
| } |
| |
| private final StoredConfig baseConfig; |
| |
| 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 Map<String, 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 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.put(commentLink.name, commentLink); |
| } |
| |
| private ProjectConfig(Project.NameKey projectName, @Nullable StoredConfig baseConfig) { |
| this.projectName = projectName; |
| this.baseConfig = baseConfig; |
| } |
| |
| 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<>()); |
| } 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.values(); |
| } |
| |
| 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 { |
| if (baseConfig != null) { |
| baseConfig.load(); |
| } |
| readGroupList(); |
| groupsByName = mapGroupReferences(); |
| |
| rulesId = getObjectId("rules.pl"); |
| Config rc = readConfig(PROJECT_CONFIG, baseConfig); |
| 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)); |
| ca.setExcludeProjectsRegexes( |
| loadPatterns(rc, CONTRIBUTOR_AGREEMENT, name, KEY_EXCLUDE_PROJECTS)); |
| ca.setMatchProjectsRegexes(loadPatterns(rc, CONTRIBUTOR_AGREEMENT, name, KEY_MATCH_PROJECTS)); |
| |
| 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 (AccessSection.isValidRefSectionName(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 (isCoreOrPluginPermission(n)) { |
| as.getPermission(n, true).setExclusiveGroup(true); |
| } |
| } |
| } |
| |
| for (String varName : rc.getNames(ACCESS, refName)) { |
| String convertedName = convertLegacyPermission(varName); |
| if (isCoreOrPluginPermission(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 isCoreOrPluginPermission(String permission) { |
| // Since plugins are loaded dynamically, here we can't load all plugin permissions and verify |
| // their existence. |
| return isPermission(permission) || isValidPluginPermission(permission); |
| } |
| |
| private boolean isValidRegex(String refPattern) { |
| try { |
| RefPattern.validateRegExp(refPattern); |
| } catch (InvalidNameException e) { |
| error(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<String> loadPatterns( |
| Config rc, String section, String subsection, String varName) { |
| ImmutableList.Builder<String> patterns = ImmutableList.builder(); |
| for (String patternString : rc.getStringList(section, subsection, varName)) { |
| try { |
| // While one could just use getStringList directly, compiling first will cause the server |
| // to fail fast if any of the patterns are invalid. |
| patterns.add(Pattern.compile(patternString).pattern()); |
| } catch (PatternSyntaxException e) { |
| error(new ValidationError(PROJECT_CONFIG, "Invalid regular expression: " + e.getMessage())); |
| continue; |
| } |
| } |
| return patterns.build(); |
| } |
| |
| 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 LinkedHashMap<>(subsections.size()); |
| for (String name : subsections) { |
| try { |
| commentLinkSections.put(name, 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.values()) { |
| 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)); |
| rc.setStringList( |
| CONTRIBUTOR_AGREEMENT, |
| ca.getName(), |
| KEY_EXCLUDE_PROJECTS, |
| patternToStringList(ca.getExcludeProjectsRegexes())); |
| rc.setStringList( |
| CONTRIBUTOR_AGREEMENT, |
| ca.getName(), |
| KEY_MATCH_PROJECTS, |
| patternToStringList(ca.getMatchProjectsRegexes())); |
| } |
| } |
| |
| private void saveNotifySections(Config rc, Set<AccountGroup.UUID> keepGroups) { |
| for (NotifyConfig nc : sort(notifySections.values())) { |
| nc.getGroups().stream() |
| .map(GroupReference::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> patternToStringList(List<String> list) { |
| return list; |
| } |
| |
| 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 (isCoreOrPluginPermission(convertLegacyPermission(varName)) |
| && !have.contains(varName.toLowerCase())) { |
| rc.unset(ACCESS, refName, varName); |
| } |
| } |
| } |
| |
| for (String name : rc.getSubsections(ACCESS)) { |
| if (AccessSection.isValidRefSectionName(name) && !accessSections.containsKey(name)) { |
| rc.unsetSection(ACCESS, name); |
| } |
| } |
| } |
| |
| private void saveLabelSections(Config rc) { |
| List<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 (Map.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()); |
| } |
| |
| @UsedAt(UsedAt.Project.GOOGLE) |
| public boolean hasLegacyPermissions() { |
| return hasLegacyPermissions; |
| } |
| |
| private String convertLegacyPermission(String permissionName) { |
| switch (permissionName) { |
| case LEGACY_PERMISSION_PUSH_TAG: |
| hasLegacyPermissions = true; |
| return Permission.CREATE_TAG; |
| case LEGACY_PERMISSION_PUSH_SIGNED_TAG: |
| hasLegacyPermissions = true; |
| return Permission.CREATE_SIGNED_TAG; |
| default: |
| return permissionName; |
| } |
| } |
| } |