Line data Source code
1 : // Copyright (C) 2022 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.schema;
16 :
17 : import static java.util.stream.Collectors.joining;
18 :
19 : import com.google.common.annotations.VisibleForTesting;
20 : import com.google.common.base.Splitter;
21 : import com.google.common.base.Strings;
22 : import com.google.common.collect.ImmutableList;
23 : import com.google.common.primitives.Shorts;
24 : import com.google.gerrit.entities.PermissionRule;
25 : import com.google.gerrit.entities.Project;
26 : import com.google.gerrit.entities.RefNames;
27 : import com.google.gerrit.extensions.client.ChangeKind;
28 : import com.google.gerrit.server.GerritPersonIdent;
29 : import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
30 : import com.google.gerrit.server.git.GitRepositoryManager;
31 : import com.google.gerrit.server.git.meta.MetaDataUpdate;
32 : import com.google.gerrit.server.project.ProjectConfig;
33 : import com.google.gerrit.server.project.ProjectLevelConfig;
34 : import com.google.inject.Inject;
35 : import java.io.IOException;
36 : import java.util.ArrayList;
37 : import java.util.Arrays;
38 : import java.util.List;
39 : import java.util.Objects;
40 : import java.util.Optional;
41 : import java.util.function.Consumer;
42 : import org.eclipse.jgit.errors.ConfigInvalidException;
43 : import org.eclipse.jgit.lib.Config;
44 : import org.eclipse.jgit.lib.PersonIdent;
45 : import org.eclipse.jgit.lib.Ref;
46 : import org.eclipse.jgit.lib.Repository;
47 : import org.eclipse.jgit.revwalk.RevCommit;
48 : import org.eclipse.jgit.revwalk.RevWalk;
49 :
50 : /**
51 : * Migrates all label configurations of a project to copy conditions.
52 : *
53 : * <p>The label configuration in {@code project.config} controls under which conditions approvals
54 : * should be copied to new patch sets:
55 : *
56 : * <ul>
57 : * <li>old way: by setting boolean flags and copy values (fields {@code copyAnyScore}, {@code
58 : * copyMinScore}, {@code copyMaxScore}, {@code copyAllScoresIfNoChange}, {@code
59 : * copyAllScoresIfNoCodeChange}, {@code copyAllScoresOnMergeFirstParentUpdate}, {@code
60 : * copyAllScoresOnTrivialRebase}, {@code copyAllScoresIfListOfFilesDidNotChange}, {@code
61 : * copyValue})
62 : * <li>new way: by setting a query as a copy condition (field {@code copyCondition})
63 : * </ul>
64 : *
65 : * <p>This class updates all label configurations in the {@code project.config} of the given
66 : * project:
67 : *
68 : * <ul>
69 : * <li>it stores the conditions under which approvals should be copied to new patchs as a copy
70 : * condition query (field {@code copyCondition})
71 : * <li>it unsets all deprecated fields to control approval copying (fields {@code copyAnyScore},
72 : * {@code copyMinScore}, {@code copyMaxScore}, {@code copyAllScoresIfNoChange}, {@code
73 : * copyAllScoresIfNoCodeChange}, {@code copyAllScoresOnMergeFirstParentUpdate}, {@code
74 : * copyAllScoresOnTrivialRebase}, {@code copyAllScoresIfListOfFilesDidNotChange}, {@code
75 : * copyValue})
76 : * </ul>
77 : *
78 : * <p>This migration assumes {@code true} as default value for the {@code copyAllScoresIfNoChange}
79 : * flag since this default value was used for all labels that were created before this migration has
80 : * been run (for labels that are created after this migration has been run the default value for
81 : * this flag has been changed to {@code false}).
82 : */
83 : public class MigrateLabelConfigToCopyCondition {
84 : public static final String COMMIT_MESSAGE = "Migrate label configs to copy conditions";
85 :
86 : @VisibleForTesting public static final String KEY_COPY_ANY_SCORE = "copyAnyScore";
87 :
88 : @VisibleForTesting public static final String KEY_COPY_MIN_SCORE = "copyMinScore";
89 :
90 : @VisibleForTesting public static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
91 :
92 : @VisibleForTesting public static final String KEY_COPY_VALUE = "copyValue";
93 :
94 : @VisibleForTesting
95 : public static final String KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE =
96 : "copyAllScoresOnMergeFirstParentUpdate";
97 :
98 : @VisibleForTesting
99 : public static final String KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = "copyAllScoresOnTrivialRebase";
100 :
101 : @VisibleForTesting
102 : public static final String KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = "copyAllScoresIfNoCodeChange";
103 :
104 : @VisibleForTesting
105 : public static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoChange";
106 :
107 : @VisibleForTesting
108 : public static final String KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE =
109 : "copyAllScoresIfListOfFilesDidNotChange";
110 :
111 : private final GitRepositoryManager repoManager;
112 : private final PersonIdent serverUser;
113 :
114 : @Inject
115 : public MigrateLabelConfigToCopyCondition(
116 1 : GitRepositoryManager repoManager, @GerritPersonIdent PersonIdent serverUser) {
117 1 : this.repoManager = repoManager;
118 1 : this.serverUser = serverUser;
119 1 : }
120 :
121 : /**
122 : * Executes the migration for the given project.
123 : *
124 : * @param projectName the name of the project for which the migration should be executed
125 : * @throws IOException thrown if an IO error occurs
126 : * @throws ConfigInvalidException thrown if the existing project.config is invalid and cannot be
127 : * parsed
128 : */
129 : public void execute(Project.NameKey projectName) throws IOException, ConfigInvalidException {
130 1 : ProjectLevelConfig.Bare projectConfig =
131 : new ProjectLevelConfig.Bare(ProjectConfig.PROJECT_CONFIG);
132 1 : try (Repository repo = repoManager.openRepository(projectName);
133 1 : MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, projectName, repo)) {
134 1 : boolean isAlreadyMigrated = hasMigrationAlreadyRun(repo);
135 :
136 1 : projectConfig.load(projectName, repo);
137 :
138 1 : Config cfg = projectConfig.getConfig();
139 1 : String orgConfigAsText = cfg.toText();
140 1 : for (String labelName : cfg.getSubsections(ProjectConfig.LABEL)) {
141 1 : String newCopyCondition = computeCopyCondition(isAlreadyMigrated, cfg, labelName);
142 1 : if (!Strings.isNullOrEmpty(newCopyCondition)) {
143 1 : cfg.setString(
144 : ProjectConfig.LABEL, labelName, ProjectConfig.KEY_COPY_CONDITION, newCopyCondition);
145 : }
146 :
147 1 : unsetDeprecatedFields(cfg, labelName);
148 1 : }
149 :
150 1 : if (cfg.toText().equals(orgConfigAsText)) {
151 : // Config was not changed (ie. none of the label definitions had any deprecated field set).
152 1 : return;
153 : }
154 :
155 1 : md.getCommitBuilder().setAuthor(serverUser);
156 1 : md.getCommitBuilder().setCommitter(serverUser);
157 1 : md.setMessage(COMMIT_MESSAGE + "\n");
158 1 : projectConfig.commit(md);
159 1 : }
160 1 : }
161 :
162 : private static String computeCopyCondition(
163 : boolean isAlreadyMigrated, Config cfg, String labelName) {
164 1 : List<String> copyConditions = new ArrayList<>();
165 :
166 1 : ifTrue(cfg, labelName, KEY_COPY_ANY_SCORE, () -> copyConditions.add("is:ANY"));
167 1 : ifTrue(cfg, labelName, KEY_COPY_MIN_SCORE, () -> copyConditions.add("is:MIN"));
168 1 : ifTrue(cfg, labelName, KEY_COPY_MAX_SCORE, () -> copyConditions.add("is:MAX"));
169 1 : forEachSkipNullValues(
170 : cfg,
171 : labelName,
172 : KEY_COPY_VALUE,
173 1 : value -> copyConditions.add("is:" + quoteIfNegative(parseCopyValue(value))));
174 1 : ifTrue(
175 : cfg,
176 : labelName,
177 : KEY_COPY_ALL_SCORES_IF_NO_CHANGE,
178 1 : () -> copyConditions.add("changekind:" + ChangeKind.NO_CHANGE.name()));
179 :
180 : // If the migration has already been run on this project we must no longer assume true as
181 : // default value when copyAllScoresIfNoChange is unset. This is to ensure that the migration is
182 : // idempotent when copyAllScoresIfNoChange is set to false:
183 : //
184 : // 1. migration run:
185 : // Removes the copyAllScoresIfNoChange flag, but doesn't add anything to the copy condition.
186 : //
187 : // 2. migration run:
188 : // Since the copyAllScoresIfNoChange flag is no longer set, we would wrongly assume true now and
189 : // wrongly include "changekind:NO_CHANGE" into the copy condition. To prevent this we assume
190 : // false as default for copyAllScoresIfNoChange once the migration has been run, so that the 2.
191 : // migration run is a no-op.
192 1 : if (!isAlreadyMigrated) {
193 : // The default value for copyAllScoresIfNoChange is true, hence if this parameter is not set
194 : // we need to include "changekind:NO_CHANGE" into the copy condition.
195 1 : ifUnset(
196 : cfg,
197 : labelName,
198 : KEY_COPY_ALL_SCORES_IF_NO_CHANGE,
199 1 : () -> copyConditions.add("changekind:" + ChangeKind.NO_CHANGE.name()));
200 : }
201 :
202 1 : ifTrue(
203 : cfg,
204 : labelName,
205 : KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE,
206 1 : () -> copyConditions.add("changekind:" + ChangeKind.NO_CODE_CHANGE.name()));
207 1 : ifTrue(
208 : cfg,
209 : labelName,
210 : KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE,
211 1 : () -> copyConditions.add("changekind:" + ChangeKind.MERGE_FIRST_PARENT_UPDATE.name()));
212 1 : ifTrue(
213 : cfg,
214 : labelName,
215 : KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE,
216 1 : () -> copyConditions.add("changekind:" + ChangeKind.TRIVIAL_REBASE.name()));
217 1 : ifTrue(
218 : cfg,
219 : labelName,
220 : KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE,
221 1 : () -> copyConditions.add("has:unchanged-files"));
222 :
223 1 : if (copyConditions.isEmpty()) {
224 : // No copy conditions need to be added. Simply return the current copy condition as it is.
225 : // Returning here prevents that OR conditions are reordered and that parentheses are added
226 : // unnecessarily.
227 1 : return cfg.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_COPY_CONDITION);
228 : }
229 :
230 1 : ifSet(
231 : cfg,
232 : labelName,
233 : ProjectConfig.KEY_COPY_CONDITION,
234 1 : copyCondition -> copyConditions.addAll(splitOrConditions(copyCondition)));
235 :
236 1 : return copyConditions.stream()
237 1 : .map(MigrateLabelConfigToCopyCondition::encloseInParenthesesIfNeeded)
238 1 : .sorted()
239 : // Remove duplicated OR conditions
240 1 : .distinct()
241 1 : .collect(joining(" OR "));
242 : }
243 :
244 : private static void ifSet(Config cfg, String labelName, String key, Consumer<String> consumer) {
245 1 : Optional.ofNullable(cfg.getString(ProjectConfig.LABEL, labelName, key)).ifPresent(consumer);
246 1 : }
247 :
248 : private static void ifUnset(Config cfg, String labelName, String key, Runnable runnable) {
249 1 : Optional<String> value =
250 1 : Optional.ofNullable(cfg.getString(ProjectConfig.LABEL, labelName, key));
251 1 : if (!value.isPresent()) {
252 1 : runnable.run();
253 : }
254 1 : }
255 :
256 : private static void ifTrue(Config cfg, String labelName, String key, Runnable runnable) {
257 1 : if (cfg.getBoolean(ProjectConfig.LABEL, labelName, key, /* defaultValue= */ false)) {
258 1 : runnable.run();
259 : }
260 1 : }
261 :
262 : private static void forEachSkipNullValues(
263 : Config cfg, String labelName, String key, Consumer<String> consumer) {
264 1 : Arrays.stream(cfg.getStringList(ProjectConfig.LABEL, labelName, key))
265 1 : .filter(Objects::nonNull)
266 1 : .forEach(consumer);
267 1 : }
268 :
269 : private static void unsetDeprecatedFields(Config cfg, String labelName) {
270 1 : cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ANY_SCORE);
271 1 : cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_MIN_SCORE);
272 1 : cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_MAX_SCORE);
273 1 : cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_VALUE);
274 1 : cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ALL_SCORES_IF_NO_CHANGE);
275 1 : cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE);
276 1 : cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE);
277 1 : cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE);
278 1 : cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE);
279 1 : }
280 :
281 : private static ImmutableList<String> splitOrConditions(String copyCondition) {
282 1 : if (copyCondition.contains("(") || copyCondition.contains(")")) {
283 : // cannot parse complex predicate tree
284 1 : return ImmutableList.of(copyCondition);
285 : }
286 :
287 : // split query on OR, this way we can detect and remove duplicate OR conditions later
288 1 : return ImmutableList.copyOf(Splitter.on(" OR ").splitToList(copyCondition));
289 : }
290 :
291 : /**
292 : * Add parentheses around the given copyCondition if it consists out of 2 or more predicates and
293 : * if it isn't enclosed in parentheses yet.
294 : */
295 : private static String encloseInParenthesesIfNeeded(String copyCondition) {
296 1 : if (copyCondition.contains(" ")
297 1 : && !(copyCondition.startsWith("(") && copyCondition.endsWith(")"))) {
298 1 : return "(" + copyCondition + ")";
299 : }
300 1 : return copyCondition;
301 : }
302 :
303 : private static short parseCopyValue(String value) {
304 1 : return Shorts.checkedCast(PermissionRule.parseInt(value));
305 : }
306 :
307 : private static String quoteIfNegative(short value) {
308 1 : if (value < 0) {
309 1 : return "\"" + value + "\"";
310 : }
311 1 : return Integer.toString(value);
312 : }
313 :
314 : public static boolean hasMigrationAlreadyRun(Repository repo) throws IOException {
315 1 : try (RevWalk revWalk = new RevWalk(repo)) {
316 1 : Ref refsMetaConfig = repo.exactRef(RefNames.REFS_CONFIG);
317 1 : if (refsMetaConfig == null) {
318 1 : return false;
319 : }
320 1 : revWalk.markStart(revWalk.parseCommit(refsMetaConfig.getObjectId()));
321 : RevCommit commit;
322 1 : while ((commit = revWalk.next()) != null) {
323 1 : if (COMMIT_MESSAGE.equals(commit.getShortMessage())) {
324 1 : return true;
325 : }
326 : }
327 1 : return false;
328 1 : }
329 : }
330 : }
|