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