Line data Source code
1 : // Copyright (C) 2008 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.git.receive;
16 :
17 : import static com.google.common.base.MoreObjects.firstNonNull;
18 : import static com.google.common.base.Preconditions.checkState;
19 : import static com.google.common.collect.ImmutableList.toImmutableList;
20 : import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
21 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
22 : import static com.google.common.flogger.LazyArgs.lazy;
23 : import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
24 : import static com.google.gerrit.entities.RefNames.isConfigRef;
25 : import static com.google.gerrit.entities.RefNames.isRefsUsersSelf;
26 : import static com.google.gerrit.git.ObjectIds.abbreviateName;
27 : import static com.google.gerrit.server.change.HashtagsUtil.cleanupHashtag;
28 : import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
29 : import static com.google.gerrit.server.git.receive.ReceiveConstants.COMMAND_REJECTION_MESSAGE_FOOTER;
30 : import static com.google.gerrit.server.git.receive.ReceiveConstants.ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP;
31 : import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION;
32 : import static com.google.gerrit.server.git.receive.ReceiveConstants.SAME_CHANGE_ID_IN_MULTIPLE_CHANGES;
33 : import static com.google.gerrit.server.git.validators.CommitValidators.NEW_PATCHSET_PATTERN;
34 : import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
35 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
36 : import static java.nio.charset.StandardCharsets.UTF_8;
37 : import static java.util.Objects.requireNonNull;
38 : import static java.util.stream.Collectors.joining;
39 : import static java.util.stream.Collectors.toList;
40 : import static org.eclipse.jgit.lib.Constants.R_HEADS;
41 : import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
42 : import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
43 : import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
44 : import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
45 :
46 : import com.google.common.base.Joiner;
47 : import com.google.common.base.Splitter;
48 : import com.google.common.base.Strings;
49 : import com.google.common.base.Throwables;
50 : import com.google.common.collect.BiMap;
51 : import com.google.common.collect.HashBiMap;
52 : import com.google.common.collect.ImmutableList;
53 : import com.google.common.collect.ImmutableListMultimap;
54 : import com.google.common.collect.ImmutableMap;
55 : import com.google.common.collect.ImmutableSet;
56 : import com.google.common.collect.ImmutableSetMultimap;
57 : import com.google.common.collect.Iterables;
58 : import com.google.common.collect.LinkedListMultimap;
59 : import com.google.common.collect.ListMultimap;
60 : import com.google.common.collect.Lists;
61 : import com.google.common.collect.Maps;
62 : import com.google.common.collect.MultimapBuilder;
63 : import com.google.common.collect.Sets;
64 : import com.google.common.collect.SortedSetMultimap;
65 : import com.google.common.collect.Streams;
66 : import com.google.common.flogger.FluentLogger;
67 : import com.google.gerrit.common.Nullable;
68 : import com.google.gerrit.common.UsedAt;
69 : import com.google.gerrit.entities.Account;
70 : import com.google.gerrit.entities.BooleanProjectConfig;
71 : import com.google.gerrit.entities.BranchNameKey;
72 : import com.google.gerrit.entities.Change;
73 : import com.google.gerrit.entities.HumanComment;
74 : import com.google.gerrit.entities.LabelType;
75 : import com.google.gerrit.entities.LabelTypes;
76 : import com.google.gerrit.entities.PatchSet;
77 : import com.google.gerrit.entities.PatchSetInfo;
78 : import com.google.gerrit.entities.Project;
79 : import com.google.gerrit.entities.RefNames;
80 : import com.google.gerrit.entities.SubmissionId;
81 : import com.google.gerrit.exceptions.StorageException;
82 : import com.google.gerrit.extensions.api.changes.HashtagsInput;
83 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
84 : import com.google.gerrit.extensions.api.changes.NotifyInfo;
85 : import com.google.gerrit.extensions.api.changes.RecipientType;
86 : import com.google.gerrit.extensions.api.changes.SubmitInput;
87 : import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
88 : import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
89 : import com.google.gerrit.extensions.registration.DynamicItem;
90 : import com.google.gerrit.extensions.registration.DynamicMap;
91 : import com.google.gerrit.extensions.registration.DynamicSet;
92 : import com.google.gerrit.extensions.registration.Extension;
93 : import com.google.gerrit.extensions.restapi.AuthException;
94 : import com.google.gerrit.extensions.restapi.BadRequestException;
95 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
96 : import com.google.gerrit.extensions.restapi.RestApiException;
97 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
98 : import com.google.gerrit.extensions.validators.CommentForValidation;
99 : import com.google.gerrit.extensions.validators.CommentForValidation.CommentSource;
100 : import com.google.gerrit.extensions.validators.CommentForValidation.CommentType;
101 : import com.google.gerrit.extensions.validators.CommentValidationContext;
102 : import com.google.gerrit.extensions.validators.CommentValidationFailure;
103 : import com.google.gerrit.extensions.validators.CommentValidator;
104 : import com.google.gerrit.metrics.Counter0;
105 : import com.google.gerrit.metrics.Counter3;
106 : import com.google.gerrit.metrics.Description;
107 : import com.google.gerrit.metrics.Field;
108 : import com.google.gerrit.metrics.MetricMaker;
109 : import com.google.gerrit.server.CancellationMetrics;
110 : import com.google.gerrit.server.ChangeUtil;
111 : import com.google.gerrit.server.CommentsUtil;
112 : import com.google.gerrit.server.CreateGroupPermissionSyncer;
113 : import com.google.gerrit.server.DeadlineChecker;
114 : import com.google.gerrit.server.IdentifiedUser;
115 : import com.google.gerrit.server.InvalidDeadlineException;
116 : import com.google.gerrit.server.PatchSetUtil;
117 : import com.google.gerrit.server.PublishCommentUtil;
118 : import com.google.gerrit.server.PublishCommentsOp;
119 : import com.google.gerrit.server.RequestInfo;
120 : import com.google.gerrit.server.RequestListener;
121 : import com.google.gerrit.server.account.AccountResolver;
122 : import com.google.gerrit.server.approval.ApprovalsUtil;
123 : import com.google.gerrit.server.cancellation.RequestCancelledException;
124 : import com.google.gerrit.server.cancellation.RequestStateContext;
125 : import com.google.gerrit.server.change.AttentionSetUnchangedOp;
126 : import com.google.gerrit.server.change.ChangeInserter;
127 : import com.google.gerrit.server.change.NotifyResolver;
128 : import com.google.gerrit.server.change.SetHashtagsOp;
129 : import com.google.gerrit.server.change.SetPrivateOp;
130 : import com.google.gerrit.server.change.SetTopicOp;
131 : import com.google.gerrit.server.config.AllProjectsName;
132 : import com.google.gerrit.server.config.GerritServerConfig;
133 : import com.google.gerrit.server.config.PluginConfig;
134 : import com.google.gerrit.server.config.ProjectConfigEntry;
135 : import com.google.gerrit.server.config.UrlFormatter;
136 : import com.google.gerrit.server.edit.ChangeEdit;
137 : import com.google.gerrit.server.edit.ChangeEditUtil;
138 : import com.google.gerrit.server.git.BanCommit;
139 : import com.google.gerrit.server.git.ChangeReportFormatter;
140 : import com.google.gerrit.server.git.GroupCollector;
141 : import com.google.gerrit.server.git.MergedByPushOp;
142 : import com.google.gerrit.server.git.MultiProgressMonitor;
143 : import com.google.gerrit.server.git.MultiProgressMonitor.Task;
144 : import com.google.gerrit.server.git.ReceivePackInitializer;
145 : import com.google.gerrit.server.git.TagCache;
146 : import com.google.gerrit.server.git.ValidationError;
147 : import com.google.gerrit.server.git.validators.CommentCountValidator;
148 : import com.google.gerrit.server.git.validators.CommentSizeValidator;
149 : import com.google.gerrit.server.git.validators.CommitValidationMessage;
150 : import com.google.gerrit.server.git.validators.RefOperationValidationException;
151 : import com.google.gerrit.server.git.validators.RefOperationValidators;
152 : import com.google.gerrit.server.git.validators.ValidationMessage;
153 : import com.google.gerrit.server.index.change.ChangeIndexer;
154 : import com.google.gerrit.server.logging.Metadata;
155 : import com.google.gerrit.server.logging.PerformanceLogContext;
156 : import com.google.gerrit.server.logging.PerformanceLogger;
157 : import com.google.gerrit.server.logging.RequestId;
158 : import com.google.gerrit.server.logging.TraceContext;
159 : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
160 : import com.google.gerrit.server.mail.MailUtil.MailRecipients;
161 : import com.google.gerrit.server.notedb.ChangeNotes;
162 : import com.google.gerrit.server.notedb.Sequences;
163 : import com.google.gerrit.server.patch.AutoMerger;
164 : import com.google.gerrit.server.patch.PatchSetInfoFactory;
165 : import com.google.gerrit.server.permissions.ChangePermission;
166 : import com.google.gerrit.server.permissions.GlobalPermission;
167 : import com.google.gerrit.server.permissions.PermissionBackend;
168 : import com.google.gerrit.server.permissions.PermissionBackendException;
169 : import com.google.gerrit.server.permissions.PermissionDeniedException;
170 : import com.google.gerrit.server.permissions.ProjectPermission;
171 : import com.google.gerrit.server.permissions.RefPermission;
172 : import com.google.gerrit.server.plugincontext.PluginSetContext;
173 : import com.google.gerrit.server.project.CreateRefControl;
174 : import com.google.gerrit.server.project.NoSuchChangeException;
175 : import com.google.gerrit.server.project.NoSuchProjectException;
176 : import com.google.gerrit.server.project.ProjectCache;
177 : import com.google.gerrit.server.project.ProjectConfig;
178 : import com.google.gerrit.server.project.ProjectState;
179 : import com.google.gerrit.server.query.change.ChangeData;
180 : import com.google.gerrit.server.query.change.InternalChangeQuery;
181 : import com.google.gerrit.server.restapi.change.ReplyAttentionSetUpdates;
182 : import com.google.gerrit.server.submit.MergeOp;
183 : import com.google.gerrit.server.submit.MergeOpRepoManager;
184 : import com.google.gerrit.server.update.BatchUpdate;
185 : import com.google.gerrit.server.update.BatchUpdateOp;
186 : import com.google.gerrit.server.update.ChangeContext;
187 : import com.google.gerrit.server.update.PostUpdateContext;
188 : import com.google.gerrit.server.update.RepoContext;
189 : import com.google.gerrit.server.update.RepoOnlyOp;
190 : import com.google.gerrit.server.update.RetryHelper;
191 : import com.google.gerrit.server.update.SubmissionExecutor;
192 : import com.google.gerrit.server.update.SubmissionListener;
193 : import com.google.gerrit.server.update.SuperprojectUpdateOnSubmission;
194 : import com.google.gerrit.server.update.UpdateException;
195 : import com.google.gerrit.server.util.LabelVote;
196 : import com.google.gerrit.server.util.MagicBranch;
197 : import com.google.gerrit.server.util.RequestScopePropagator;
198 : import com.google.gerrit.server.util.time.TimeUtil;
199 : import com.google.gerrit.util.cli.CmdLineParser;
200 : import com.google.inject.Inject;
201 : import com.google.inject.Provider;
202 : import com.google.inject.Singleton;
203 : import com.google.inject.assistedinject.Assisted;
204 : import com.google.inject.util.Providers;
205 : import java.io.IOException;
206 : import java.io.StringWriter;
207 : import java.io.UnsupportedEncodingException;
208 : import java.net.URLDecoder;
209 : import java.util.ArrayList;
210 : import java.util.Arrays;
211 : import java.util.Collection;
212 : import java.util.Collections;
213 : import java.util.HashMap;
214 : import java.util.HashSet;
215 : import java.util.Iterator;
216 : import java.util.LinkedHashMap;
217 : import java.util.LinkedHashSet;
218 : import java.util.List;
219 : import java.util.Map;
220 : import java.util.Objects;
221 : import java.util.Optional;
222 : import java.util.Queue;
223 : import java.util.Set;
224 : import java.util.TreeSet;
225 : import java.util.concurrent.ConcurrentLinkedQueue;
226 : import java.util.concurrent.ExecutionException;
227 : import java.util.concurrent.Future;
228 : import java.util.stream.Collectors;
229 : import java.util.stream.Stream;
230 : import java.util.stream.StreamSupport;
231 : import org.eclipse.jgit.errors.ConfigInvalidException;
232 : import org.eclipse.jgit.errors.IncorrectObjectTypeException;
233 : import org.eclipse.jgit.errors.MissingObjectException;
234 : import org.eclipse.jgit.lib.Config;
235 : import org.eclipse.jgit.lib.Constants;
236 : import org.eclipse.jgit.lib.ObjectId;
237 : import org.eclipse.jgit.lib.ObjectInserter;
238 : import org.eclipse.jgit.lib.ObjectReader;
239 : import org.eclipse.jgit.lib.PersonIdent;
240 : import org.eclipse.jgit.lib.Ref;
241 : import org.eclipse.jgit.lib.Repository;
242 : import org.eclipse.jgit.notes.NoteMap;
243 : import org.eclipse.jgit.revwalk.FooterLine;
244 : import org.eclipse.jgit.revwalk.RevCommit;
245 : import org.eclipse.jgit.revwalk.RevObject;
246 : import org.eclipse.jgit.revwalk.RevSort;
247 : import org.eclipse.jgit.revwalk.RevWalk;
248 : import org.eclipse.jgit.revwalk.filter.RevFilter;
249 : import org.eclipse.jgit.transport.ReceiveCommand;
250 : import org.eclipse.jgit.transport.ReceivePack;
251 : import org.kohsuke.args4j.CmdLineException;
252 : import org.kohsuke.args4j.Option;
253 :
254 : /**
255 : * Receives change upload using the Git receive-pack protocol.
256 : *
257 : * <p>Conceptually, most use of Gerrit is a push of some commits to refs/for/BRANCH. However, the
258 : * receive-pack protocol that this is based on allows multiple ref updates to be processed at once.
259 : * So we have to be prepared to also handle normal pushes (refs/heads/BRANCH), and legacy pushes
260 : * (refs/changes/CHANGE). It is hard to split this class up further, because normal pushes can also
261 : * result in updates to reviews, through the autoclose mechanism.
262 : */
263 : class ReceiveCommits {
264 97 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
265 :
266 : private static final String CANNOT_DELETE_CHANGES = "Cannot delete from '" + REFS_CHANGES + "'";
267 : private static final String CANNOT_DELETE_CONFIG =
268 : "Cannot delete project configuration from '" + RefNames.REFS_CONFIG + "'";
269 : private static final String INTERNAL_SERVER_ERROR = "internal server error";
270 :
271 : interface Factory {
272 : ReceiveCommits create(
273 : ProjectState projectState,
274 : IdentifiedUser user,
275 : ReceivePack receivePack,
276 : Repository repository,
277 : AllRefsWatcher allRefsWatcher,
278 : MessageSender messageSender);
279 : }
280 :
281 97 : private class ReceivePackMessageSender implements MessageSender {
282 : @Override
283 : public void sendMessage(String what) {
284 90 : receivePack.sendMessage(what);
285 90 : }
286 :
287 : @Override
288 : public void sendError(String what) {
289 0 : receivePack.sendError(what);
290 0 : }
291 :
292 : @Override
293 : public void sendBytes(byte[] what) {
294 96 : sendBytes(what, 0, what.length);
295 96 : }
296 :
297 : @Override
298 : public void sendBytes(byte[] what, int off, int len) {
299 : try {
300 96 : receivePack.getMessageOutputStream().write(what, off, len);
301 0 : } catch (IOException e) {
302 : // Ignore write failures (matching JGit behavior).
303 96 : }
304 96 : }
305 :
306 : @Override
307 : public void flush() {
308 : try {
309 96 : receivePack.getMessageOutputStream().flush();
310 0 : } catch (IOException e) {
311 : // Ignore write failures (matching JGit behavior).
312 96 : }
313 96 : }
314 : }
315 :
316 : private static RestApiException asRestApiException(Exception e) {
317 3 : if (e instanceof RestApiException) {
318 0 : return (RestApiException) e;
319 3 : } else if ((e instanceof ExecutionException) && (e.getCause() instanceof RestApiException)) {
320 0 : return (RestApiException) e.getCause();
321 : }
322 3 : return RestApiException.wrap("Error inserting change/patchset", e);
323 : }
324 :
325 : @Singleton
326 : private static class Metrics {
327 : private final Counter0 psRevisionMissing;
328 : private final Counter3<String, String, String> pushCount;
329 :
330 : @Inject
331 97 : Metrics(MetricMaker metricMaker) {
332 97 : psRevisionMissing =
333 97 : metricMaker.newCounter(
334 : "receivecommits/ps_revision_missing",
335 : new Description("errors due to patch set revision missing"));
336 97 : pushCount =
337 97 : metricMaker.newCounter(
338 : "receivecommits/push_count",
339 : new Description("number of pushes"),
340 97 : Field.ofString("kind", (metadataBuilder, fieldValue) -> {})
341 97 : .description("The push kind (direct vs. magic).")
342 97 : .build(),
343 97 : Field.ofString(
344 : "project",
345 0 : (metadataBuilder, fieldValue) -> metadataBuilder.projectName(fieldValue))
346 97 : .description("The name of the project for which the push is done.")
347 97 : .build(),
348 97 : Field.ofString("type", (metadataBuilder, fieldValue) -> {})
349 97 : .description(
350 : "The type of the update (CREATE, UPDATE, CREATE/UPDATE,"
351 : + " UPDATE_NONFASTFORWARD, DELETE).")
352 97 : .build());
353 97 : }
354 : }
355 :
356 : // ReceiveCommits has a lot of fields, sorry. Here and in the constructor they are split up
357 : // somewhat, and kept sorted lexicographically within sections, except where later assignments
358 : // depend on previous ones.
359 :
360 : // Injected fields.
361 : private final AccountResolver accountResolver;
362 : private final AllProjectsName allProjectsName;
363 : private final BatchUpdate.Factory batchUpdateFactory;
364 : private final CancellationMetrics cancellationMetrics;
365 : private final ChangeEditUtil editUtil;
366 : private final ChangeIndexer indexer;
367 : private final ChangeInserter.Factory changeInserterFactory;
368 : private final ChangeNotes.Factory notesFactory;
369 : private final ChangeReportFormatter changeFormatter;
370 : private final CmdLineParser.Factory optionParserFactory;
371 : private final CommentsUtil commentsUtil;
372 : private final PluginSetContext<CommentValidator> commentValidators;
373 : private final BranchCommitValidator.Factory commitValidatorFactory;
374 : private final Config config;
375 : private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
376 : private final CreateRefControl createRefControl;
377 : private final DeadlineChecker.Factory deadlineCheckerFactory;
378 : private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
379 : private final DynamicSet<PluginPushOption> pluginPushOptions;
380 : private final PluginSetContext<ReceivePackInitializer> initializers;
381 : private final MergedByPushOp.Factory mergedByPushOpFactory;
382 : private final Metrics metrics;
383 : private final PatchSetInfoFactory patchSetInfoFactory;
384 : private final PatchSetUtil psUtil;
385 : private final DynamicSet<PerformanceLogger> performanceLoggers;
386 : private final PermissionBackend permissionBackend;
387 : private final ProjectCache projectCache;
388 : private final Provider<InternalChangeQuery> queryProvider;
389 : private final Provider<MergeOp> mergeOpProvider;
390 : private final Provider<MergeOpRepoManager> ormProvider;
391 : private final ReceiveConfig receiveConfig;
392 : private final RefOperationValidators.Factory refValidatorsFactory;
393 : private final ReplaceOp.Factory replaceOpFactory;
394 : private final PluginSetContext<RequestListener> requestListeners;
395 : private final PublishCommentsOp.Factory publishCommentsOp;
396 : private final RetryHelper retryHelper;
397 : private final RequestScopePropagator requestScopePropagator;
398 : private final Sequences seq;
399 : private final SetHashtagsOp.Factory hashtagsFactory;
400 : private final SetTopicOp.Factory setTopicFactory;
401 : private final ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners;
402 : private final TagCache tagCache;
403 : private final ProjectConfig.Factory projectConfigFactory;
404 : private final SetPrivateOp.Factory setPrivateOpFactory;
405 : private final ReplyAttentionSetUpdates replyAttentionSetUpdates;
406 : private final DynamicItem<UrlFormatter> urlFormatter;
407 : private final AutoMerger autoMerger;
408 :
409 : // Assisted injected fields.
410 : private final ProjectState projectState;
411 : private final IdentifiedUser user;
412 : private final ReceivePack receivePack;
413 :
414 : // Immutable fields derived from constructor arguments.
415 : private final boolean allowProjectOwnersToChangeParent;
416 : private final LabelTypes labelTypes;
417 : private final NoteMap rejectCommits;
418 : private final PermissionBackend.ForProject permissions;
419 : private final Project project;
420 : private final Repository repo;
421 :
422 : // Collections populated during processing.
423 : private final List<UpdateGroupsRequest> updateGroups;
424 : private final Queue<ValidationMessage> messages;
425 : /** Multimap of error text to refnames that produced that error. */
426 : private final ListMultimap<String, String> errors;
427 :
428 : private final ListMultimap<String, String> pushOptions;
429 : private final ReceivePackRefCache receivePackRefCache;
430 : private final Map<Change.Id, ReplaceRequest> replaceByChange;
431 :
432 : // Other settings populated during processing.
433 : private MagicBranchInput magicBranch;
434 : private boolean newChangeForAllNotInTarget;
435 : private boolean setChangeAsPrivate;
436 : private Optional<NoteDbPushOption> noteDbPushOption;
437 : private Optional<String> tracePushOption;
438 :
439 : private MessageSender messageSender;
440 : private ReceiveCommitsResult.Builder result;
441 : private ImmutableMap<String, String> loggingTags;
442 : private ImmutableList<String> transitionalPluginOptions;
443 :
444 : /** This object is for single use only. */
445 : private boolean used;
446 :
447 : @Inject
448 : ReceiveCommits(
449 : AccountResolver accountResolver,
450 : AllProjectsName allProjectsName,
451 : BatchUpdate.Factory batchUpdateFactory,
452 : CancellationMetrics cancellationMetrics,
453 : ProjectConfig.Factory projectConfigFactory,
454 : @GerritServerConfig Config config,
455 : ChangeEditUtil editUtil,
456 : ChangeIndexer indexer,
457 : ChangeInserter.Factory changeInserterFactory,
458 : ChangeNotes.Factory notesFactory,
459 : DynamicItem<ChangeReportFormatter> changeFormatterProvider,
460 : CmdLineParser.Factory optionParserFactory,
461 : CommentsUtil commentsUtil,
462 : BranchCommitValidator.Factory commitValidatorFactory,
463 : CreateGroupPermissionSyncer createGroupPermissionSyncer,
464 : CreateRefControl createRefControl,
465 : DeadlineChecker.Factory deadlineCheckerFactory,
466 : DynamicMap<ProjectConfigEntry> pluginConfigEntries,
467 : DynamicSet<PluginPushOption> pluginPushOptions,
468 : PluginSetContext<ReceivePackInitializer> initializers,
469 : PluginSetContext<CommentValidator> commentValidators,
470 : MergedByPushOp.Factory mergedByPushOpFactory,
471 : Metrics metrics,
472 : PatchSetInfoFactory patchSetInfoFactory,
473 : PatchSetUtil psUtil,
474 : DynamicSet<PerformanceLogger> performanceLoggers,
475 : PermissionBackend permissionBackend,
476 : ProjectCache projectCache,
477 : Provider<InternalChangeQuery> queryProvider,
478 : Provider<MergeOp> mergeOpProvider,
479 : Provider<MergeOpRepoManager> ormProvider,
480 : PublishCommentsOp.Factory publishCommentsOp,
481 : ReceiveConfig receiveConfig,
482 : RefOperationValidators.Factory refValidatorsFactory,
483 : ReplaceOp.Factory replaceOpFactory,
484 : PluginSetContext<RequestListener> requestListeners,
485 : RetryHelper retryHelper,
486 : RequestScopePropagator requestScopePropagator,
487 : Sequences seq,
488 : SetHashtagsOp.Factory hashtagsFactory,
489 : SetTopicOp.Factory setTopicFactory,
490 : @SuperprojectUpdateOnSubmission
491 : ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners,
492 : TagCache tagCache,
493 : SetPrivateOp.Factory setPrivateOpFactory,
494 : ReplyAttentionSetUpdates replyAttentionSetUpdates,
495 : DynamicItem<UrlFormatter> urlFormatter,
496 : AutoMerger autoMerger,
497 : @Assisted ProjectState projectState,
498 : @Assisted IdentifiedUser user,
499 : @Assisted ReceivePack rp,
500 : @Assisted Repository repository,
501 : @Assisted AllRefsWatcher allRefsWatcher,
502 : @Nullable @Assisted MessageSender messageSender)
503 97 : throws IOException {
504 : // Injected fields.
505 97 : this.accountResolver = accountResolver;
506 97 : this.allProjectsName = allProjectsName;
507 97 : this.batchUpdateFactory = batchUpdateFactory;
508 97 : this.cancellationMetrics = cancellationMetrics;
509 97 : this.changeFormatter = changeFormatterProvider.get();
510 97 : this.changeInserterFactory = changeInserterFactory;
511 97 : this.commentsUtil = commentsUtil;
512 97 : this.commentValidators = commentValidators;
513 97 : this.commitValidatorFactory = commitValidatorFactory;
514 97 : this.config = config;
515 97 : this.createRefControl = createRefControl;
516 97 : this.createGroupPermissionSyncer = createGroupPermissionSyncer;
517 97 : this.deadlineCheckerFactory = deadlineCheckerFactory;
518 97 : this.editUtil = editUtil;
519 97 : this.hashtagsFactory = hashtagsFactory;
520 97 : this.setTopicFactory = setTopicFactory;
521 97 : this.indexer = indexer;
522 97 : this.initializers = initializers;
523 97 : this.mergeOpProvider = mergeOpProvider;
524 97 : this.mergedByPushOpFactory = mergedByPushOpFactory;
525 97 : this.notesFactory = notesFactory;
526 97 : this.optionParserFactory = optionParserFactory;
527 97 : this.ormProvider = ormProvider;
528 97 : this.metrics = metrics;
529 97 : this.patchSetInfoFactory = patchSetInfoFactory;
530 97 : this.permissionBackend = permissionBackend;
531 97 : this.pluginConfigEntries = pluginConfigEntries;
532 97 : this.pluginPushOptions = pluginPushOptions;
533 97 : this.projectCache = projectCache;
534 97 : this.psUtil = psUtil;
535 97 : this.performanceLoggers = performanceLoggers;
536 97 : this.publishCommentsOp = publishCommentsOp;
537 97 : this.queryProvider = queryProvider;
538 97 : this.receiveConfig = receiveConfig;
539 97 : this.refValidatorsFactory = refValidatorsFactory;
540 97 : this.replaceOpFactory = replaceOpFactory;
541 97 : this.requestListeners = requestListeners;
542 97 : this.retryHelper = retryHelper;
543 97 : this.requestScopePropagator = requestScopePropagator;
544 97 : this.seq = seq;
545 97 : this.superprojectUpdateSubmissionListeners = superprojectUpdateSubmissionListeners;
546 97 : this.tagCache = tagCache;
547 97 : this.projectConfigFactory = projectConfigFactory;
548 97 : this.setPrivateOpFactory = setPrivateOpFactory;
549 97 : this.replyAttentionSetUpdates = replyAttentionSetUpdates;
550 97 : this.urlFormatter = urlFormatter;
551 97 : this.autoMerger = autoMerger;
552 :
553 : // Assisted injected fields.
554 97 : this.projectState = projectState;
555 97 : this.user = user;
556 97 : this.receivePack = rp;
557 : // This repository instance in unwrapped, while the repository instance in
558 : // receivePack.getRepo() is wrapped in PermissionAwareRepository instance.
559 97 : this.repo = repository;
560 :
561 : // Immutable fields derived from constructor arguments.
562 97 : project = projectState.getProject();
563 97 : labelTypes = projectState.getLabelTypes();
564 97 : permissions = permissionBackend.user(user).project(project.getNameKey());
565 97 : rejectCommits = BanCommit.loadRejectCommitsMap(repo, rp.getRevWalk());
566 :
567 : // Collections populated during processing.
568 97 : errors = MultimapBuilder.linkedHashKeys().arrayListValues().build();
569 97 : messages = new ConcurrentLinkedQueue<>();
570 97 : pushOptions = LinkedListMultimap.create();
571 97 : replaceByChange = new LinkedHashMap<>();
572 97 : updateGroups = new ArrayList<>();
573 :
574 97 : used = false;
575 :
576 97 : this.allowProjectOwnersToChangeParent =
577 97 : config.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
578 :
579 : // Other settings populated during processing.
580 97 : newChangeForAllNotInTarget =
581 97 : projectState.is(BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET);
582 :
583 : // Handles for outputting back over the wire to the end user.
584 97 : this.messageSender = messageSender != null ? messageSender : new ReceivePackMessageSender();
585 97 : this.result = ReceiveCommitsResult.builder();
586 97 : this.loggingTags = ImmutableMap.of();
587 :
588 : // TODO(hiesel): Make this decision implicit once vetted
589 97 : boolean useRefCache = config.getBoolean("receive", "enableInMemoryRefCache", true);
590 97 : receivePackRefCache =
591 97 : useRefCache
592 97 : ? ReceivePackRefCache.withAdvertisedRefs(() -> allRefsWatcher.getAllRefs())
593 97 : : ReceivePackRefCache.noCache(receivePack.getRepository().getRefDatabase());
594 97 : this.transitionalPluginOptions =
595 97 : ImmutableList.copyOf(config.getStringList("plugins", null, "transitionalPushOptions"));
596 97 : }
597 :
598 : void init() {
599 97 : initializers.runEach(i -> i.init(projectState.getNameKey(), receivePack));
600 97 : }
601 :
602 : MessageSender getMessageSender() {
603 96 : return messageSender;
604 : }
605 :
606 : Project getProject() {
607 96 : return project;
608 : }
609 :
610 : private void addMessage(String message, ValidationMessage.Type type) {
611 6 : messages.add(new CommitValidationMessage(message, type));
612 6 : }
613 :
614 : private void addMessage(String message) {
615 88 : messages.add(new CommitValidationMessage(message, ValidationMessage.Type.OTHER));
616 88 : }
617 :
618 : private void addError(String error) {
619 5 : addMessage(error, ValidationMessage.Type.ERROR);
620 5 : }
621 :
622 : /**
623 : * Sends all messages which have been collected while processing the push to the client.
624 : *
625 : * <p><strong>Attention:</strong>{@link AsyncReceiveCommits} may call this method while {@link
626 : * #processCommands(Collection, MultiProgressMonitor)} is still running (if the execution of
627 : * processCommands takes too long and AsyncReceiveCommits gets a timeout). This means that local
628 : * variables that are accessed in this method must be thread-safe (otherwise we may hit a {@link
629 : * java.util.ConcurrentModificationException} if we read a variable here that at the same time is
630 : * updated by the background thread that still executes processCommands).
631 : */
632 : void sendMessages() {
633 96 : try (TraceContext traceContext =
634 96 : TraceContext.newTrace(
635 96 : loggingTags.containsKey(RequestId.Type.TRACE_ID.name()),
636 96 : loggingTags.get(RequestId.Type.TRACE_ID.name()),
637 4 : (tagName, traceId) -> {})) {
638 96 : loggingTags.forEach((tagName, tagValue) -> traceContext.addTag(tagName, tagValue));
639 :
640 96 : for (ValidationMessage m : messages) {
641 90 : String msg = m.getType().getPrefix() + m.getMessage();
642 90 : logger.atFine().log("Sending message: %s", msg);
643 :
644 : // Avoid calling sendError which will add its own error: prefix.
645 90 : messageSender.sendMessage(msg);
646 90 : }
647 : }
648 96 : }
649 :
650 : ReceiveCommitsResult processCommands(
651 : Collection<ReceiveCommand> commands, MultiProgressMonitor progress) throws StorageException {
652 96 : checkState(!used, "Tried to re-use a ReceiveCommits objects that is single-use only");
653 96 : long start = TimeUtil.nowNanos();
654 96 : parsePushOptions();
655 96 : String clientProvidedDeadlineValue =
656 96 : Iterables.getLast(pushOptions.get("deadline"), /* defaultValue= */ null);
657 96 : int commandCount = commands.size();
658 96 : try (TraceContext traceContext =
659 96 : TraceContext.newTrace(
660 96 : tracePushOption.isPresent(),
661 96 : tracePushOption.orElse(null),
662 4 : (tagName, traceId) -> addMessage(tagName + ": " + traceId));
663 96 : PerformanceLogContext performanceLogContext =
664 : new PerformanceLogContext(config, performanceLoggers);
665 96 : TraceTimer traceTimer =
666 96 : newTimer("processCommands", Metadata.builder().resourceCount(commandCount))) {
667 96 : RequestInfo requestInfo =
668 96 : RequestInfo.builder(RequestInfo.RequestType.GIT_RECEIVE, user, traceContext)
669 96 : .project(project.getNameKey())
670 96 : .build();
671 96 : requestListeners.runEach(l -> l.onRequest(requestInfo));
672 96 : traceContext.addTag(RequestId.Type.RECEIVE_ID, new RequestId(project.getNameKey().get()));
673 :
674 : // Log the push options here, rather than in parsePushOptions(), so that they are included
675 : // into the trace if tracing is enabled.
676 96 : logger.atFine().log("push options: %s", receivePack.getPushOptions());
677 :
678 96 : Task commandProgress = progress.beginSubTask("refs", UNKNOWN);
679 96 : commands =
680 96 : commands.stream().map(c -> wrapReceiveCommand(c, commandProgress)).collect(toList());
681 :
682 : try (RequestStateContext requestStateContext =
683 96 : RequestStateContext.open()
684 96 : .addRequestStateProvider(progress)
685 96 : .addRequestStateProvider(
686 96 : deadlineCheckerFactory.create(start, requestInfo, clientProvidedDeadlineValue))) {
687 96 : processCommandsUnsafe(commands, progress);
688 96 : rejectRemaining(commands, INTERNAL_SERVER_ERROR);
689 1 : } catch (InvalidDeadlineException e) {
690 1 : rejectRemaining(commands, e.getMessage());
691 4 : } catch (RuntimeException e) {
692 4 : Optional<RequestCancelledException> requestCancelledException =
693 4 : RequestCancelledException.getFromCausalChain(e);
694 4 : if (!requestCancelledException.isPresent()) {
695 0 : Throwables.throwIfUnchecked(e);
696 : }
697 1 : cancellationMetrics.countCancelledRequest(
698 1 : requestInfo, requestCancelledException.get().getCancellationReason());
699 1 : StringBuilder msg =
700 1 : new StringBuilder(requestCancelledException.get().formatCancellationReason());
701 1 : if (requestCancelledException.get().getCancellationMessage().isPresent()) {
702 1 : msg.append(
703 1 : String.format(
704 1 : " (%s)", requestCancelledException.get().getCancellationMessage().get()));
705 : }
706 1 : rejectRemaining(commands, msg.toString());
707 96 : }
708 :
709 : // This sends error messages before the 'done' string of the progress monitor is sent.
710 : // Currently, the test framework relies on this ordering to understand if pushes completed
711 : // successfully.
712 96 : sendErrorMessages();
713 :
714 96 : commandProgress.end();
715 96 : loggingTags = traceContext.getTags();
716 96 : logger.atFine().log("Processing commands done.");
717 : }
718 96 : progress.end();
719 96 : return result.build();
720 : }
721 :
722 : // Process as many commands as possible, but may leave some commands in state NOT_ATTEMPTED.
723 : private void processCommandsUnsafe(
724 : Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
725 96 : logger.atFine().log("Calling user: %s, commands: %d", user.getLoggableName(), commands.size());
726 :
727 : // If the list of groups is large, the log entry may get dropped, so separate out.
728 96 : logger.atFine().log("Groups: %s", lazy(() -> user.getEffectiveGroups().getKnownGroups()));
729 :
730 96 : if (!projectState.getProject().getState().permitsWrite()) {
731 1 : for (ReceiveCommand cmd : commands) {
732 1 : reject(cmd, "prohibited by Gerrit: project state does not permit write");
733 1 : }
734 1 : return;
735 : }
736 :
737 96 : List<ReceiveCommand> magicCommands = new ArrayList<>();
738 96 : List<ReceiveCommand> regularCommands = new ArrayList<>();
739 :
740 96 : for (ReceiveCommand cmd : commands) {
741 96 : if (MagicBranch.isMagicBranch(cmd.getRefName())) {
742 89 : magicCommands.add(cmd);
743 : } else {
744 49 : regularCommands.add(cmd);
745 : }
746 96 : }
747 :
748 96 : if (!magicCommands.isEmpty() && !regularCommands.isEmpty()) {
749 1 : rejectRemaining(commands, "cannot combine normal pushes and magic pushes");
750 1 : return;
751 : }
752 :
753 : try {
754 96 : if (!magicCommands.isEmpty()) {
755 89 : parseMagicBranch(Iterables.getLast(magicCommands));
756 : // Using the submit option submits the created change(s) immediately without checking labels
757 : // nor submit rules. Hence we shouldn't record such pushes as "magic" which implies that
758 : // code review is being done.
759 89 : String pushKind = magicBranch != null && magicBranch.submit ? "direct_submit" : "magic";
760 89 : metrics.pushCount.increment(pushKind, project.getName(), getUpdateType(magicCommands));
761 : }
762 96 : if (!regularCommands.isEmpty()) {
763 49 : metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
764 : }
765 :
766 96 : if (!regularCommands.isEmpty()) {
767 49 : handleRegularCommands(regularCommands, progress);
768 49 : return;
769 : }
770 :
771 89 : boolean first = true;
772 89 : for (ReceiveCommand cmd : magicCommands) {
773 89 : if (first) {
774 89 : first = false;
775 : } else {
776 0 : reject(cmd, "duplicate request");
777 : }
778 89 : }
779 0 : } catch (PermissionBackendException | NoSuchProjectException | IOException err) {
780 0 : logger.atSevere().withCause(err).log("Failed to process refs in %s", project.getName());
781 0 : return;
782 89 : }
783 :
784 89 : Task newProgress = progress.beginSubTask("new", UNKNOWN);
785 89 : Task replaceProgress = progress.beginSubTask("updated", UNKNOWN);
786 :
787 89 : ImmutableList<CreateRequest> newChanges = ImmutableList.of();
788 : try {
789 89 : if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
790 : try {
791 89 : newChanges = selectNewAndReplacedChangesFromMagicBranch(newProgress);
792 0 : } catch (IOException e) {
793 0 : throw new StorageException("Failed to select new changes in " + project.getName(), e);
794 89 : }
795 : }
796 :
797 : // Commit validation has already happened, so any changes without Change-Id are for the
798 : // deprecated feature.
799 89 : warnAboutMissingChangeId(newChanges);
800 89 : preparePatchSetsForReplace(newChanges);
801 89 : insertChangesAndPatchSets(newChanges, replaceProgress);
802 : } finally {
803 89 : newProgress.end();
804 89 : replaceProgress.end();
805 : }
806 :
807 89 : queueSuccessMessages(newChanges);
808 :
809 89 : logger.atFine().log(
810 : "Command results: %s",
811 89 : lazy(() -> commands.stream().map(ReceiveCommits::commandToString).collect(joining(","))));
812 89 : }
813 :
814 : private String getUpdateType(List<ReceiveCommand> commands) {
815 96 : return commands.stream()
816 96 : .map(ReceiveCommand::getType)
817 96 : .map(ReceiveCommand.Type::name)
818 96 : .distinct()
819 96 : .sorted()
820 96 : .collect(joining("/"));
821 : }
822 :
823 : private void sendErrorMessages() {
824 96 : if (!errors.isEmpty()) {
825 12 : logger.atFine().log("Handling error conditions: %s", errors.keySet());
826 12 : for (String error : errors.keySet()) {
827 12 : receivePack.sendMessage("error: " + buildError(error, errors.get(error)));
828 12 : }
829 12 : receivePack.sendMessage(String.format("User: %s", user.getLoggableName()));
830 12 : receivePack.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
831 : }
832 96 : }
833 :
834 : private void handleRegularCommands(List<ReceiveCommand> cmds, MultiProgressMonitor progress)
835 : throws PermissionBackendException, IOException, NoSuchProjectException {
836 49 : try (TraceTimer traceTimer =
837 49 : newTimer("handleRegularCommands", Metadata.builder().resourceCount(cmds.size()))) {
838 49 : result.magicPush(false);
839 49 : for (ReceiveCommand cmd : cmds) {
840 49 : parseRegularCommand(cmd);
841 49 : }
842 :
843 : Map<BranchNameKey, ReceiveCommand> branches;
844 49 : try (BatchUpdate bu =
845 49 : batchUpdateFactory.create(
846 49 : project.getNameKey(), user.materializedCopy(), TimeUtil.now());
847 49 : ObjectInserter ins = repo.newObjectInserter();
848 49 : ObjectReader reader = ins.newReader();
849 49 : RevWalk rw = new RevWalk(reader);
850 49 : MergeOpRepoManager orm = ormProvider.get()) {
851 49 : bu.setRepository(repo, rw, ins);
852 49 : bu.setRefLogMessage("push");
853 :
854 49 : int added = 0;
855 49 : for (ReceiveCommand cmd : cmds) {
856 49 : if (cmd.getResult() == NOT_ATTEMPTED) {
857 47 : bu.addRepoOnlyOp(new UpdateOneRefOp(cmd));
858 47 : added++;
859 : }
860 49 : }
861 49 : logger.atFine().log("Added %d additional ref updates", added);
862 :
863 49 : SubmissionExecutor submissionExecutor =
864 : new SubmissionExecutor(false, superprojectUpdateSubmissionListeners);
865 :
866 49 : submissionExecutor.execute(ImmutableList.of(bu));
867 :
868 49 : orm.setContext(TimeUtil.now(), user, NotifyResolver.Result.none());
869 49 : submissionExecutor.afterExecutions(orm);
870 :
871 49 : branches = bu.getSuccessfullyUpdatedBranches(false);
872 0 : } catch (UpdateException | RestApiException e) {
873 0 : throw new StorageException(e);
874 49 : }
875 :
876 : // This could be moved into a SubmissionListener
877 49 : branches.values().stream()
878 49 : .filter(c -> isHead(c) || isConfig(c))
879 49 : .forEach(
880 : c -> {
881 : // Most post-update steps should happen in UpdateOneRefOp#postUpdate. The only steps
882 : // that should happen in this loops are things that can't happen within one
883 : // BatchUpdate because they involve kicking off an additional BatchUpdate.
884 38 : switch (c.getType()) {
885 : case CREATE:
886 : case UPDATE:
887 : case UPDATE_NONFASTFORWARD:
888 38 : Task closeProgress = progress.beginSubTask("closed", UNKNOWN);
889 38 : autoCloseChanges(c, closeProgress);
890 38 : closeProgress.end();
891 38 : break;
892 :
893 : case DELETE:
894 : break;
895 : }
896 38 : });
897 : }
898 49 : }
899 :
900 : /** Appends messages for successful change creation/updates. */
901 : private void queueSuccessMessages(List<CreateRequest> newChanges) {
902 : // adjacency list for commit => parent
903 89 : Map<String, String> adjList = new HashMap<>();
904 89 : for (CreateRequest cr : newChanges) {
905 88 : String parent = cr.commit.getParentCount() == 0 ? null : cr.commit.getParent(0).name();
906 88 : adjList.put(cr.commit.name(), parent);
907 88 : }
908 89 : for (ReplaceRequest rr : replaceByChange.values()) {
909 38 : String parent = null;
910 38 : if (rr.revCommit != null) {
911 38 : parent = rr.revCommit.getParentCount() == 0 ? null : rr.revCommit.getParent(0).name();
912 : }
913 38 : adjList.put(rr.newCommitId.name(), parent);
914 38 : }
915 :
916 : // get commits that are not parents
917 89 : Set<String> leafs = new TreeSet<>(adjList.keySet());
918 89 : leafs.removeAll(adjList.values());
919 : // go backwards from the last commit to its parent(s)
920 89 : Set<String> ordered = new LinkedHashSet<>();
921 89 : for (String leaf : leafs) {
922 88 : if (ordered.contains(leaf)) {
923 0 : continue;
924 : }
925 88 : while (leaf != null) {
926 88 : if (!ordered.contains(leaf)) {
927 88 : ordered.add(leaf);
928 : }
929 88 : leaf = adjList.get(leaf);
930 : }
931 88 : }
932 : // reverse the order to start with earliest commit
933 89 : List<String> orderedCommits = new ArrayList<>(ordered);
934 89 : Collections.reverse(orderedCommits);
935 :
936 89 : Map<String, CreateRequest> created =
937 89 : newChanges.stream()
938 89 : .filter(r -> r.change != null)
939 89 : .collect(Collectors.toMap(r -> r.commit.name(), r -> r));
940 89 : Map<String, ReplaceRequest> updated =
941 89 : replaceByChange.values().stream()
942 89 : .filter(r -> r.inputCommand.getResult() == OK)
943 89 : .collect(Collectors.toMap(r -> r.newCommitId.name(), r -> r));
944 :
945 89 : if (created.isEmpty() && updated.isEmpty()) {
946 14 : return;
947 : }
948 :
949 88 : addMessage("");
950 88 : addMessage("SUCCESS");
951 88 : addMessage("");
952 :
953 88 : boolean edit = false;
954 88 : Boolean isPrivate = null;
955 88 : Boolean wip = null;
956 88 : if (!updated.isEmpty()) {
957 37 : edit = magicBranch != null && magicBranch.edit;
958 37 : if (magicBranch != null) {
959 37 : if (magicBranch.isPrivate) {
960 1 : isPrivate = true;
961 37 : } else if (magicBranch.removePrivate) {
962 3 : isPrivate = false;
963 : }
964 37 : if (magicBranch.workInProgress) {
965 5 : wip = true;
966 37 : } else if (magicBranch.ready) {
967 4 : wip = false;
968 : }
969 : }
970 : }
971 :
972 88 : for (String commit : orderedCommits) {
973 88 : if (created.get(commit) != null) {
974 88 : addCreatedMessage(created.get(commit));
975 88 : } else if (updated.get(commit) != null) {
976 37 : addReplacedMessage(updated.get(commit), edit, isPrivate, wip);
977 : }
978 88 : }
979 88 : addMessage("");
980 88 : }
981 :
982 : private void addCreatedMessage(CreateRequest c) {
983 88 : addMessage(
984 88 : changeFormatter.newChange(
985 88 : ChangeReportFormatter.Input.builder().setChange(c.change).build()));
986 88 : }
987 :
988 : private void addReplacedMessage(ReplaceRequest u, boolean edit, Boolean isPrivate, Boolean wip) {
989 : String subject;
990 37 : if (edit) {
991 : subject =
992 3 : u.revCommit == null ? u.notes.getChange().getSubject() : u.revCommit.getShortMessage();
993 : } else {
994 37 : subject = u.info.getSubject();
995 : }
996 :
997 37 : if (isPrivate == null) {
998 37 : isPrivate = u.notes.getChange().isPrivate();
999 : }
1000 37 : if (wip == null) {
1001 37 : wip = u.notes.getChange().isWorkInProgress();
1002 : }
1003 :
1004 : ChangeReportFormatter.Input input =
1005 37 : ChangeReportFormatter.Input.builder()
1006 37 : .setChange(u.notes.getChange())
1007 37 : .setSubject(subject)
1008 37 : .setIsEdit(edit)
1009 37 : .setIsPrivate(isPrivate)
1010 37 : .setIsWorkInProgress(wip)
1011 37 : .build();
1012 37 : addMessage(changeFormatter.changeUpdated(input));
1013 37 : u.getOutdatedApprovalsMessage().map(msg -> "\n" + msg + "\n").ifPresent(this::addMessage);
1014 37 : }
1015 :
1016 : private void insertChangesAndPatchSets(
1017 : ImmutableList<CreateRequest> newChanges, Task replaceProgress) {
1018 89 : try (TraceTimer traceTimer =
1019 89 : newTimer(
1020 89 : "insertChangesAndPatchSets", Metadata.builder().resourceCount(newChanges.size()))) {
1021 89 : ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null;
1022 89 : if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) {
1023 9 : logger.atWarning().log(
1024 : "Skipping change updates on %s because ref update failed: %s %s",
1025 9 : project.getName(),
1026 9 : magicBranchCmd.getResult(),
1027 9 : Strings.nullToEmpty(magicBranchCmd.getMessage()));
1028 9 : return;
1029 : }
1030 :
1031 88 : try (BatchUpdate bu =
1032 88 : batchUpdateFactory.create(
1033 88 : project.getNameKey(), user.materializedCopy(), TimeUtil.now());
1034 88 : ObjectInserter ins = repo.newObjectInserter();
1035 88 : ObjectReader reader = ins.newReader();
1036 88 : RevWalk rw = new RevWalk(reader)) {
1037 88 : bu.setRepository(repo, rw, ins);
1038 88 : bu.setRefLogMessage("push");
1039 88 : if (magicBranch != null) {
1040 88 : bu.setNotify(magicBranch.getNotifyForNewChange());
1041 : }
1042 :
1043 88 : logger.atFine().log("Adding %d replace requests", newChanges.size());
1044 88 : for (ReplaceRequest replace : replaceByChange.values()) {
1045 37 : replace.addOps(bu, replaceProgress);
1046 37 : if (magicBranch != null) {
1047 37 : bu.setNotifyHandling(replace.ontoChange, magicBranch.getNotifyHandling(replace.notes));
1048 37 : if (magicBranch.shouldPublishComments()) {
1049 4 : bu.addOp(
1050 4 : replace.notes.getChangeId(),
1051 4 : publishCommentsOp.create(replace.psId, project.getNameKey()));
1052 4 : Optional<ChangeNotes> changeNotes = getChangeNotes(replace.notes.getChangeId());
1053 4 : if (!changeNotes.isPresent()) {
1054 : // If not present, no need to update attention set here since this is a new change.
1055 0 : continue;
1056 : }
1057 4 : List<HumanComment> drafts =
1058 4 : commentsUtil.draftByChangeAuthor(changeNotes.get(), user.getAccountId());
1059 4 : if (drafts.isEmpty()) {
1060 : // If no comments, attention set shouldn't update since the user didn't reply.
1061 1 : continue;
1062 : }
1063 4 : replyAttentionSetUpdates.processAutomaticAttentionSetRulesOnReply(
1064 4 : bu, changeNotes.get(), isReadyForReview(changeNotes.get()), user, drafts);
1065 : }
1066 : }
1067 37 : }
1068 :
1069 88 : logger.atFine().log("Adding %d create requests", newChanges.size());
1070 88 : for (CreateRequest create : newChanges) {
1071 88 : create.addOps(bu);
1072 88 : }
1073 :
1074 88 : logger.atFine().log("Adding %d group update requests", newChanges.size());
1075 88 : updateGroups.forEach(r -> r.addOps(bu));
1076 :
1077 88 : logger.atFine().log("Executing batch");
1078 : try {
1079 88 : bu.execute();
1080 3 : } catch (UpdateException e) {
1081 3 : throw asRestApiException(e);
1082 88 : }
1083 :
1084 88 : replaceByChange.values().stream()
1085 88 : .forEach(
1086 : req ->
1087 37 : result.addChange(ReceiveCommitsResult.ChangeStatus.REPLACED, req.ontoChange));
1088 88 : newChanges.stream()
1089 88 : .forEach(
1090 88 : req -> result.addChange(ReceiveCommitsResult.ChangeStatus.CREATED, req.changeId));
1091 :
1092 88 : if (magicBranchCmd != null) {
1093 88 : magicBranchCmd.setResult(OK);
1094 : }
1095 88 : for (ReplaceRequest replace : replaceByChange.values()) {
1096 37 : String rejectMessage = replace.getRejectMessage();
1097 37 : if (rejectMessage == null) {
1098 37 : if (replace.inputCommand.getResult() == NOT_ATTEMPTED) {
1099 : // Not necessarily the magic branch, so need to set OK on the original value.
1100 0 : replace.inputCommand.setResult(OK);
1101 : }
1102 : } else {
1103 0 : logger.atFine().log("Rejecting due to message from ReplaceOp");
1104 0 : reject(replace.inputCommand, rejectMessage);
1105 : }
1106 37 : }
1107 :
1108 0 : } catch (ResourceConflictException e) {
1109 0 : addError(e.getMessage());
1110 0 : reject(magicBranchCmd, "conflict");
1111 3 : } catch (BadRequestException | UnprocessableEntityException | AuthException e) {
1112 3 : logger.atFine().withCause(e).log("Rejecting due to client error");
1113 3 : reject(magicBranchCmd, e.getMessage());
1114 3 : } catch (RestApiException | IOException e) {
1115 3 : throw new StorageException("Can't insert change/patch set for " + project.getName(), e);
1116 88 : }
1117 :
1118 88 : if (magicBranch != null && magicBranch.submit) {
1119 : try {
1120 8 : submit(newChanges, replaceByChange.values());
1121 2 : } catch (ResourceConflictException e) {
1122 2 : addError(e.getMessage());
1123 2 : reject(magicBranchCmd, "conflict");
1124 0 : } catch (RestApiException
1125 : | StorageException
1126 : | UpdateException
1127 : | IOException
1128 : | ConfigInvalidException
1129 : | PermissionBackendException e) {
1130 0 : logger.atSevere().withCause(e).log("Error submitting changes to %s", project.getName());
1131 0 : reject(magicBranchCmd, "error during submit");
1132 8 : }
1133 : }
1134 9 : }
1135 88 : }
1136 :
1137 : private boolean isReadyForReview(ChangeNotes changeNotes) {
1138 4 : return (!changeNotes.getChange().isWorkInProgress() && !magicBranch.workInProgress)
1139 : || magicBranch.ready;
1140 : }
1141 :
1142 : private String buildError(String error, List<String> branches) {
1143 12 : StringBuilder sb = new StringBuilder();
1144 12 : if (branches.size() == 1) {
1145 12 : String branch = branches.get(0);
1146 12 : sb.append("branch ").append(branch).append(":\n");
1147 : // As of 2020, there are still many git-review <1.27 installations in the wild.
1148 : // These users will see failures as their old git-review assumes that
1149 : // `refs/publish/...` is still magic, which it isn't. As Gerrit's default error messages are
1150 : // misleading for these users, we hint them at upgrading their git-review.
1151 12 : if (branch.startsWith("refs/publish/")) {
1152 0 : sb.append("If you are using git-review, update to at least git-review 1.27. Otherwise:\n");
1153 : }
1154 12 : sb.append(error);
1155 12 : return sb.toString();
1156 : }
1157 1 : sb.append("branches ").append(Joiner.on(", ").join(branches));
1158 1 : return sb.append(":\n").append(error).toString();
1159 : }
1160 :
1161 : /** Parses push options specified as "git push -o OPTION" */
1162 : private void parsePushOptions() {
1163 96 : List<String> optionList = receivePack.getPushOptions();
1164 96 : if (optionList != null) {
1165 7 : for (String option : optionList) {
1166 6 : int e = option.indexOf('=');
1167 6 : if (e > 0) {
1168 6 : pushOptions.put(option.substring(0, e), option.substring(e + 1));
1169 : } else {
1170 5 : pushOptions.put(option, "");
1171 : }
1172 6 : }
1173 : }
1174 :
1175 96 : List<String> noteDbValues = pushOptions.get("notedb");
1176 96 : if (!noteDbValues.isEmpty()) {
1177 : // These semantics for duplicates/errors are somewhat arbitrary and may not match e.g. the
1178 : // CmdLineParser behavior used by MagicBranchInput.
1179 4 : String value = Iterables.getLast(noteDbValues);
1180 4 : noteDbPushOption = NoteDbPushOption.parse(value);
1181 4 : if (!noteDbPushOption.isPresent()) {
1182 3 : addError("Invalid value in -o " + NoteDbPushOption.OPTION_NAME + "=" + value);
1183 : }
1184 4 : } else {
1185 96 : noteDbPushOption = Optional.of(NoteDbPushOption.DISALLOW);
1186 : }
1187 :
1188 96 : List<String> traceValues = pushOptions.get("trace");
1189 96 : if (!traceValues.isEmpty()) {
1190 4 : tracePushOption = Optional.of(Iterables.getLast(traceValues));
1191 : } else {
1192 96 : tracePushOption = Optional.empty();
1193 : }
1194 96 : }
1195 :
1196 : // Wrap ReceiveCommand so the progress counter works automatically.
1197 : private ReceiveCommand wrapReceiveCommand(ReceiveCommand cmd, Task progress) {
1198 96 : String refname = cmd.getRefName();
1199 :
1200 96 : if (isRefsUsersSelf(cmd.getRefName(), projectState.isAllUsers())) {
1201 2 : refname = RefNames.refsUsers(user.getAccountId());
1202 2 : logger.atFine().log("Swapping out command for %s to %s", RefNames.REFS_USERS_SELF, refname);
1203 : }
1204 :
1205 : // We must also update the original, because callers may inspect it afterwards to decide if
1206 : // the command went through or not.
1207 96 : return new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), refname, cmd.getType()) {
1208 : @Override
1209 : public void setResult(Result s, String m) {
1210 96 : if (getResult() == NOT_ATTEMPTED) { // Only report the progress update once.
1211 96 : progress.update(1);
1212 : }
1213 : // Counter intuitively, we don't check that results == NOT_ATTEMPTED here.
1214 : // This is so submit-on-push can still reject the update if the change is created
1215 : // successfully
1216 : // (status OK) but the submit failed (merge failed: REJECTED_OTHER_REASON).
1217 96 : super.setResult(s, m);
1218 96 : cmd.setResult(s, m);
1219 96 : }
1220 : };
1221 : }
1222 :
1223 : /*
1224 : * Interpret a normal push.
1225 : */
1226 : private void parseRegularCommand(ReceiveCommand cmd)
1227 : throws PermissionBackendException, NoSuchProjectException, IOException {
1228 49 : try (TraceTimer traceTimer = newTimer("parseRegularCommand")) {
1229 49 : if (cmd.getResult() != NOT_ATTEMPTED) {
1230 : // Already rejected by the core receive process.
1231 0 : logger.atFine().log("Already processed by core: %s %s", cmd.getResult(), cmd);
1232 0 : return;
1233 : }
1234 :
1235 49 : if (!Repository.isValidRefName(cmd.getRefName()) || cmd.getRefName().contains("//")) {
1236 0 : reject(cmd, "not valid ref");
1237 0 : return;
1238 : }
1239 49 : if (RefNames.isNoteDbMetaRef(cmd.getRefName())) {
1240 : // Reject pushes to NoteDb refs without a special option and permission. Note that this
1241 : // prohibition doesn't depend on NoteDb being enabled in any way, since all sites will
1242 : // migrate to NoteDb eventually, and we don't want garbage data waiting there when the
1243 : // migration finishes.
1244 4 : logger.atFine().log(
1245 : "%s NoteDb ref %s with %s=%s",
1246 4 : cmd.getType(), cmd.getRefName(), NoteDbPushOption.OPTION_NAME, noteDbPushOption);
1247 4 : if (!Optional.of(NoteDbPushOption.ALLOW).equals(noteDbPushOption)) {
1248 : // Only reject this command, not the whole push. This supports the use case of "git clone
1249 : // --mirror" followed by "git push --mirror", when the user doesn't really intend to clone
1250 : // or mirror the NoteDb data; there is no single refspec that describes all refs *except*
1251 : // NoteDb refs.
1252 3 : reject(
1253 : cmd,
1254 : "NoteDb update requires -o "
1255 : + NoteDbPushOption.OPTION_NAME
1256 : + "="
1257 3 : + NoteDbPushOption.ALLOW.value());
1258 3 : return;
1259 : }
1260 4 : if (!permissionBackend.user(user).test(GlobalPermission.ACCESS_DATABASE)) {
1261 4 : reject(cmd, "NoteDb update requires access database permission");
1262 4 : return;
1263 : }
1264 : }
1265 :
1266 49 : switch (cmd.getType()) {
1267 : case CREATE:
1268 29 : parseCreate(cmd);
1269 29 : break;
1270 :
1271 : case UPDATE:
1272 44 : parseUpdate(cmd);
1273 44 : break;
1274 :
1275 : case DELETE:
1276 11 : parseDelete(cmd);
1277 11 : break;
1278 :
1279 : case UPDATE_NONFASTFORWARD:
1280 13 : parseRewind(cmd);
1281 13 : break;
1282 :
1283 : default:
1284 0 : reject(cmd, "prohibited by Gerrit: unknown command type " + cmd.getType());
1285 0 : return;
1286 : }
1287 :
1288 49 : if (cmd.getResult() != NOT_ATTEMPTED) {
1289 22 : return;
1290 : }
1291 :
1292 47 : if (isConfig(cmd)) {
1293 5 : validateConfigPush(cmd);
1294 : }
1295 22 : }
1296 47 : }
1297 :
1298 : /** Validates a push to refs/meta/config, and reject the command if it fails. */
1299 : private void validateConfigPush(ReceiveCommand cmd) throws PermissionBackendException {
1300 5 : try (TraceTimer traceTimer = newTimer("validateConfigPush")) {
1301 5 : logger.atFine().log("Processing %s command", cmd.getRefName());
1302 5 : if (!permissions.test(ProjectPermission.WRITE_CONFIG)) {
1303 0 : reject(
1304 : cmd,
1305 0 : String.format(
1306 : "must be either project owner or have %s permission",
1307 0 : ProjectPermission.WRITE_CONFIG.describeForException()));
1308 0 : return;
1309 : }
1310 :
1311 5 : switch (cmd.getType()) {
1312 : case CREATE:
1313 : case UPDATE:
1314 : case UPDATE_NONFASTFORWARD:
1315 : try {
1316 5 : ProjectConfig cfg = projectConfigFactory.create(project.getNameKey());
1317 5 : cfg.load(project.getNameKey(), receivePack.getRevWalk(), cmd.getNewId());
1318 5 : if (!cfg.getValidationErrors().isEmpty()) {
1319 0 : addError("Invalid project configuration:");
1320 0 : for (ValidationError err : cfg.getValidationErrors()) {
1321 0 : addError(" " + err.getMessage());
1322 0 : }
1323 0 : reject(cmd, "invalid project configuration");
1324 0 : logger.atSevere().log(
1325 : "User %s tried to push invalid project configuration %s for %s",
1326 0 : user.getLoggableName(), cmd.getNewId().name(), project.getName());
1327 0 : return;
1328 : }
1329 5 : Project.NameKey newParent = cfg.getProject().getParent(allProjectsName);
1330 5 : Project.NameKey oldParent = project.getParent(allProjectsName);
1331 5 : if (oldParent == null) {
1332 : // update of the 'All-Projects' project
1333 1 : if (newParent != null) {
1334 0 : reject(cmd, "invalid project configuration: root project cannot have parent");
1335 0 : return;
1336 : }
1337 : } else {
1338 5 : if (!oldParent.equals(newParent)) {
1339 1 : if (allowProjectOwnersToChangeParent) {
1340 0 : if (!permissionBackend
1341 0 : .user(user)
1342 0 : .project(project.getNameKey())
1343 0 : .test(ProjectPermission.WRITE_CONFIG)) {
1344 0 : reject(
1345 : cmd, "invalid project configuration: only project owners can set parent");
1346 0 : return;
1347 : }
1348 : } else {
1349 1 : if (!permissionBackend.user(user).test(GlobalPermission.ADMINISTRATE_SERVER)) {
1350 1 : reject(cmd, "invalid project configuration: only Gerrit admin can set parent");
1351 1 : return;
1352 : }
1353 : }
1354 : }
1355 :
1356 5 : if (!projectCache.get(newParent).isPresent()) {
1357 0 : reject(cmd, "invalid project configuration: parent does not exist");
1358 0 : return;
1359 : }
1360 : }
1361 5 : validatePluginConfig(cmd, cfg);
1362 0 : } catch (Exception e) {
1363 0 : reject(cmd, "invalid project configuration");
1364 0 : logger.atSevere().withCause(e).log(
1365 : "User %s tried to push invalid project configuration %s for %s",
1366 0 : user.getLoggableName(), cmd.getNewId().name(), project.getName());
1367 0 : return;
1368 5 : }
1369 : break;
1370 :
1371 : case DELETE:
1372 0 : break;
1373 :
1374 : default:
1375 0 : reject(
1376 : cmd,
1377 : "prohibited by Gerrit: don't know how to handle config update of type "
1378 0 : + cmd.getType());
1379 : }
1380 1 : }
1381 5 : }
1382 :
1383 : /**
1384 : * validates a push to refs/meta/config for plugin configuration, and rejects the push if it
1385 : * fails.
1386 : */
1387 : private void validatePluginConfig(ReceiveCommand cmd, ProjectConfig cfg) {
1388 5 : for (Extension<ProjectConfigEntry> e : pluginConfigEntries) {
1389 0 : PluginConfig pluginCfg = cfg.getPluginConfig(e.getPluginName());
1390 0 : ProjectConfigEntry configEntry = e.getProvider().get();
1391 0 : String value = pluginCfg.getString(e.getExportName());
1392 0 : String oldValue =
1393 0 : projectState.getPluginConfig(e.getPluginName()).getString(e.getExportName());
1394 0 : if (configEntry.getType() == ProjectConfigEntryType.ARRAY) {
1395 0 : oldValue =
1396 0 : Arrays.stream(
1397 : projectState
1398 0 : .getPluginConfig(e.getPluginName())
1399 0 : .getStringList(e.getExportName()))
1400 0 : .collect(joining("\n"));
1401 : }
1402 :
1403 0 : if ((value == null ? oldValue != null : !value.equals(oldValue))
1404 0 : && !configEntry.isEditable(projectState)) {
1405 0 : reject(
1406 : cmd,
1407 0 : String.format(
1408 : "invalid project configuration: Not allowed to set parameter"
1409 : + " '%s' of plugin '%s' on project '%s'.",
1410 0 : e.getExportName(), e.getPluginName(), project.getName()));
1411 0 : continue;
1412 : }
1413 :
1414 0 : if (ProjectConfigEntryType.LIST.equals(configEntry.getType())
1415 : && value != null
1416 0 : && !configEntry.getPermittedValues().contains(value)) {
1417 0 : reject(
1418 : cmd,
1419 0 : String.format(
1420 : "invalid project configuration: The value '%s' is "
1421 : + "not permitted for parameter '%s' of plugin '%s'.",
1422 0 : value, e.getExportName(), e.getPluginName()));
1423 : }
1424 0 : }
1425 5 : }
1426 :
1427 : private void parseCreate(ReceiveCommand cmd)
1428 : throws PermissionBackendException, NoSuchProjectException, IOException {
1429 29 : try (TraceTimer traceTimer = newTimer("parseCreate")) {
1430 29 : if (repo.resolve(cmd.getRefName()) != null) {
1431 8 : reject(
1432 : cmd,
1433 8 : String.format("Cannot create ref '%s' because it already exists.", cmd.getRefName()));
1434 8 : return;
1435 : }
1436 : RevObject obj;
1437 : try {
1438 29 : obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
1439 0 : } catch (IOException e) {
1440 0 : throw new StorageException(
1441 0 : String.format(
1442 0 : "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName()),
1443 : e);
1444 29 : }
1445 29 : logger.atFine().log("Creating %s", cmd);
1446 :
1447 29 : if (isHead(cmd) && !isCommit(cmd)) {
1448 0 : return;
1449 : }
1450 :
1451 29 : BranchNameKey branch = BranchNameKey.create(project.getName(), cmd.getRefName());
1452 : try {
1453 : // Must pass explicit user instead of injecting a provider into CreateRefControl, since
1454 : // Provider<CurrentUser> within ReceiveCommits will always return anonymous.
1455 29 : createRefControl.checkCreateRef(
1456 29 : Providers.of(user), receivePack.getRepository(), branch, obj, /* forPush= */ true);
1457 8 : } catch (AuthException denied) {
1458 8 : rejectProhibited(cmd, denied);
1459 8 : return;
1460 0 : } catch (ResourceConflictException denied) {
1461 0 : reject(cmd, "prohibited by Gerrit: " + denied.getMessage());
1462 0 : return;
1463 29 : }
1464 :
1465 29 : if (validRefOperation(cmd)) {
1466 29 : validateRegularPushCommits(
1467 29 : BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
1468 : }
1469 9 : }
1470 29 : }
1471 :
1472 : private void parseUpdate(ReceiveCommand cmd) throws PermissionBackendException {
1473 44 : try (TraceTimer traceTimer = TraceContext.newTimer("parseUpdate")) {
1474 44 : logger.atFine().log("Updating %s", cmd);
1475 44 : Optional<AuthException> err = checkRefPermission(cmd, RefPermission.UPDATE);
1476 44 : if (!err.isPresent()) {
1477 44 : if (isHead(cmd) && !isCommit(cmd)) {
1478 0 : reject(cmd, "head must point to commit");
1479 0 : return;
1480 : }
1481 44 : if (validRefOperation(cmd)) {
1482 44 : validateRegularPushCommits(
1483 44 : BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
1484 : }
1485 : } else {
1486 3 : rejectProhibited(cmd, err.get());
1487 : }
1488 0 : }
1489 44 : }
1490 :
1491 : private boolean isCommit(ReceiveCommand cmd) {
1492 : RevObject obj;
1493 : try {
1494 36 : obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
1495 0 : } catch (IOException e) {
1496 0 : throw new StorageException(
1497 0 : String.format(
1498 0 : "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName()),
1499 : e);
1500 36 : }
1501 :
1502 36 : if (obj instanceof RevCommit) {
1503 36 : return true;
1504 : }
1505 0 : reject(cmd, "not a commit");
1506 0 : return false;
1507 : }
1508 :
1509 : private void parseDelete(ReceiveCommand cmd) throws PermissionBackendException {
1510 11 : try (TraceTimer traceTimer = newTimer("parseDelete")) {
1511 11 : logger.atFine().log("Deleting %s", cmd);
1512 11 : if (cmd.getRefName().startsWith(REFS_CHANGES)) {
1513 0 : errors.put(CANNOT_DELETE_CHANGES, cmd.getRefName());
1514 0 : reject(cmd, "cannot delete changes");
1515 11 : } else if (isConfigRef(cmd.getRefName())) {
1516 0 : errors.put(CANNOT_DELETE_CONFIG, cmd.getRefName());
1517 0 : reject(cmd, "cannot delete project configuration");
1518 : }
1519 :
1520 11 : Optional<AuthException> err = checkRefPermission(cmd, RefPermission.DELETE);
1521 11 : if (!err.isPresent()) {
1522 10 : validRefOperation(cmd);
1523 : } else {
1524 7 : rejectProhibited(cmd, err.get());
1525 : }
1526 : }
1527 11 : }
1528 :
1529 : private void parseRewind(ReceiveCommand cmd) throws PermissionBackendException {
1530 13 : try (TraceTimer traceTimer = newTimer("parseRewind")) {
1531 : try {
1532 13 : receivePack.getRevWalk().parseCommit(cmd.getNewId());
1533 0 : } catch (IOException e) {
1534 0 : throw new StorageException(
1535 0 : String.format(
1536 0 : "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName()),
1537 : e);
1538 13 : }
1539 13 : logger.atFine().log("Rewinding %s", cmd);
1540 :
1541 13 : if (!validRefOperation(cmd)) {
1542 1 : return;
1543 : }
1544 12 : validateRegularPushCommits(BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
1545 12 : if (cmd.getResult() != NOT_ATTEMPTED) {
1546 0 : return;
1547 : }
1548 :
1549 12 : Optional<AuthException> err = checkRefPermission(cmd, RefPermission.FORCE_UPDATE);
1550 12 : if (err.isPresent()) {
1551 7 : rejectProhibited(cmd, err.get());
1552 : }
1553 1 : }
1554 12 : }
1555 :
1556 : private Optional<AuthException> checkRefPermission(ReceiveCommand cmd, RefPermission perm)
1557 : throws PermissionBackendException {
1558 49 : return checkRefPermission(permissions.ref(cmd.getRefName()), perm);
1559 : }
1560 :
1561 : private Optional<AuthException> checkRefPermission(
1562 : PermissionBackend.ForRef forRef, RefPermission perm) throws PermissionBackendException {
1563 : try {
1564 96 : forRef.check(perm);
1565 96 : return Optional.empty();
1566 12 : } catch (AuthException e) {
1567 12 : return Optional.of(e);
1568 : }
1569 : }
1570 :
1571 : private void rejectProhibited(ReceiveCommand cmd, AuthException err) {
1572 12 : err.getAdvice().ifPresent(a -> errors.put(a, cmd.getRefName()));
1573 12 : reject(cmd, prohibited(err, cmd.getRefName()));
1574 12 : }
1575 :
1576 : private static String prohibited(AuthException e, String alreadyDisplayedResource) {
1577 12 : String msg = e.getMessage();
1578 12 : if (e instanceof PermissionDeniedException) {
1579 12 : PermissionDeniedException pde = (PermissionDeniedException) e;
1580 12 : if (pde.getResource().isPresent()
1581 12 : && pde.getResource().get().equals(alreadyDisplayedResource)) {
1582 : // Avoid repeating resource name if exactly the given name was already displayed by the
1583 : // generic git push machinery.
1584 10 : msg = PermissionDeniedException.MESSAGE_PREFIX + pde.describePermission();
1585 : }
1586 : }
1587 12 : return "prohibited by Gerrit: " + msg;
1588 : }
1589 :
1590 : static class MagicBranchInput {
1591 152 : private static final Splitter COMMAS = Splitter.on(',').omitEmptyStrings();
1592 :
1593 : private final IdentifiedUser user;
1594 : private final ProjectState projectState;
1595 : private final boolean defaultPublishComments;
1596 :
1597 : final ReceiveCommand cmd;
1598 : final LabelTypes labelTypes;
1599 : /**
1600 : * Draft comments are published with the commit iff {@code --publish-comments} is set. All
1601 : * drafts are withheld (overriding the option) if at least one of the following conditions are
1602 : * met:
1603 : *
1604 : * <ul>
1605 : * <li>Installed {@link CommentValidator} plugins reject one or more draft comments.
1606 : * <li>One or more comments exceed the maximum comment size (see {@link
1607 : * CommentSizeValidator}).
1608 : * <li>The maximum number of comments would be exceeded (see {@link CommentCountValidator}).
1609 : * </ul>
1610 : */
1611 89 : private boolean withholdComments = false;
1612 :
1613 : BranchNameKey dest;
1614 : PermissionBackend.ForRef perm;
1615 89 : Set<String> reviewer = Sets.newLinkedHashSet();
1616 89 : Set<String> cc = Sets.newLinkedHashSet();
1617 89 : Map<String, Short> labels = new HashMap<>();
1618 : String message;
1619 : List<RevCommit> baseCommit;
1620 : CmdLineParser cmdLineParser;
1621 89 : Set<String> hashtags = new HashSet<>();
1622 :
1623 : @Option(name = "--trace", metaVar = "NAME", usage = "enable tracing")
1624 : String trace;
1625 :
1626 : @Option(
1627 : name = "--deadline",
1628 : metaVar = "NAME",
1629 : usage = "deadline after which the push should be aborted")
1630 : String deadline;
1631 :
1632 : @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
1633 : List<ObjectId> base;
1634 :
1635 : @Option(name = "--topic", metaVar = "NAME", usage = "attach topic to changes")
1636 : String topic;
1637 :
1638 : @Option(name = "--private", usage = "mark new/updated change as private")
1639 : boolean isPrivate;
1640 :
1641 : @Option(name = "--remove-private", usage = "remove privacy flag from updated change")
1642 : boolean removePrivate;
1643 :
1644 : /**
1645 : * The skip-validation option is defined to allow parsing it using the {@link #cmdLineParser}.
1646 : * However we do not allow this option for pushes to magic branches. This option is used to fail
1647 : * with a proper error message.
1648 : */
1649 : @Option(name = "--skip-validation", usage = "skips commit validation")
1650 : boolean skipValidation;
1651 :
1652 : @Option(
1653 : name = "--wip",
1654 : aliases = {"-work-in-progress"},
1655 : usage = "mark change as work in progress")
1656 : boolean workInProgress;
1657 :
1658 : @Option(name = "--ready", usage = "mark change as ready")
1659 : boolean ready;
1660 :
1661 : @Option(
1662 : name = "--edit",
1663 : aliases = {"-e"},
1664 : usage = "upload as change edit")
1665 : boolean edit;
1666 :
1667 : @Option(name = "--submit", usage = "immediately submit the change")
1668 : boolean submit;
1669 :
1670 : @Option(name = "--merged", usage = "create single change for a merged commit")
1671 : boolean merged;
1672 :
1673 : @Option(name = "--publish-comments", usage = "publish all draft comments on updated changes")
1674 : private boolean publishComments;
1675 :
1676 : @Option(
1677 : name = "--no-publish-comments",
1678 : aliases = {"--np"},
1679 : usage = "do not publish draft comments")
1680 : private boolean noPublishComments;
1681 :
1682 : @Option(
1683 : name = "--notify",
1684 : usage =
1685 : "Notify handling that defines to whom email notifications "
1686 : + "should be sent. Allowed values are NONE, OWNER, "
1687 : + "OWNER_REVIEWERS, ALL. If not set, the default is ALL.")
1688 : private NotifyHandling notifyHandling;
1689 :
1690 89 : @Option(
1691 : name = "--notify-to",
1692 : metaVar = "USER",
1693 : usage = "user that should be notified one time by email")
1694 : List<Account.Id> notifyTo = new ArrayList<>();
1695 :
1696 89 : @Option(
1697 : name = "--notify-cc",
1698 : metaVar = "USER",
1699 : usage = "user that should be CC'd one time by email")
1700 : List<Account.Id> notifyCc = new ArrayList<>();
1701 :
1702 89 : @Option(
1703 : name = "--notify-bcc",
1704 : metaVar = "USER",
1705 : usage = "user that should be BCC'd one time by email")
1706 : List<Account.Id> notifyBcc = new ArrayList<>();
1707 :
1708 : @Option(
1709 : name = "--reviewer",
1710 : aliases = {"-r"},
1711 : metaVar = "REVIEWER",
1712 : usage = "add reviewer to changes")
1713 : void reviewer(String str) {
1714 6 : reviewer.add(str);
1715 6 : }
1716 :
1717 : @Option(name = "--cc", metaVar = "CC", usage = "add CC to changes")
1718 : void cc(String str) {
1719 6 : cc.add(str);
1720 6 : }
1721 :
1722 : @Option(
1723 : name = "--label",
1724 : aliases = {"-l"},
1725 : metaVar = "LABEL+VALUE",
1726 : usage = "label(s) to assign (defaults to +1 if no value provided)")
1727 : void addLabel(String token) throws CmdLineException {
1728 4 : LabelVote v = LabelVote.parse(token);
1729 : try {
1730 4 : LabelType.checkName(v.label());
1731 4 : ApprovalsUtil.checkLabel(labelTypes, v.label(), v.value());
1732 3 : } catch (BadRequestException e) {
1733 3 : throw cmdLineParser.reject(e.getMessage());
1734 4 : }
1735 4 : labels.put(v.label(), v.value());
1736 4 : }
1737 :
1738 : @Option(
1739 : name = "--message",
1740 : aliases = {"-m"},
1741 : metaVar = "MESSAGE",
1742 : usage = "Comment message to apply to the review")
1743 : void addMessage(String token) {
1744 : // Many characters have special meaning in the context of a git ref.
1745 : //
1746 : // Clients can use underscores to represent spaces.
1747 4 : message = token.replace("_", " ");
1748 : try {
1749 : // Other characters can be represented using percent-encoding.
1750 4 : message = URLDecoder.decode(message, UTF_8.name());
1751 3 : } catch (IllegalArgumentException e) {
1752 : // Ignore decoding errors; leave message as percent-encoded.
1753 0 : } catch (UnsupportedEncodingException e) {
1754 : // This shouldn't happen; surely URLDecoder recognizes UTF-8.
1755 0 : throw new IllegalStateException(e);
1756 4 : }
1757 4 : }
1758 :
1759 : @Option(
1760 : name = "--hashtag",
1761 : aliases = {"-t"},
1762 : metaVar = "HASHTAG",
1763 : usage = "add hashtag to changes")
1764 : void addHashtag(String token) {
1765 3 : String hashtag = cleanupHashtag(token);
1766 3 : if (!hashtag.isEmpty()) {
1767 3 : hashtags.add(hashtag);
1768 : }
1769 3 : }
1770 :
1771 : @UsedAt(UsedAt.Project.GOOGLE)
1772 : @SuppressWarnings("unused") // unused in upstream, but used at Google
1773 : @Option(name = "--create-cod-token", usage = "create a token for consistency-on-demand")
1774 : private boolean createCodToken;
1775 :
1776 : @Option(
1777 : name = "--ignore-automatic-attention-set-rules",
1778 : aliases = {"-ias", "-ignore-attention-set"},
1779 : usage = "do not change the attention set on this push")
1780 : boolean ignoreAttentionSet;
1781 :
1782 : MagicBranchInput(
1783 89 : IdentifiedUser user, ProjectState projectState, ReceiveCommand cmd, LabelTypes labelTypes) {
1784 89 : this.user = user;
1785 89 : this.projectState = projectState;
1786 89 : this.cmd = cmd;
1787 89 : this.labelTypes = labelTypes;
1788 89 : GeneralPreferencesInfo prefs = user.state().generalPreferences();
1789 89 : this.defaultPublishComments =
1790 89 : prefs != null
1791 89 : ? firstNonNull(user.state().generalPreferences().publishCommentsOnPush, false)
1792 89 : : false;
1793 89 : }
1794 :
1795 : /**
1796 : * Get reviewer strings from magic branch options, combined with additional recipients computed
1797 : * from some other place.
1798 : *
1799 : * <p>The set of reviewers on a change includes strings passed explicitly via options as well as
1800 : * account IDs computed from the commit message itself.
1801 : *
1802 : * @param additionalRecipients recipients parsed from the commit.
1803 : * @return set of reviewer strings to pass to {@code ReviewerModifier}.
1804 : */
1805 : ImmutableSet<String> getCombinedReviewers(MailRecipients additionalRecipients) {
1806 88 : return getCombinedReviewers(reviewer, additionalRecipients.getReviewers());
1807 : }
1808 :
1809 : /**
1810 : * Get CC strings from magic branch options, combined with additional recipients computed from
1811 : * some other place.
1812 : *
1813 : * <p>The set of CCs on a change includes strings passed explicitly via options as well as
1814 : * account IDs computed from the commit message itself.
1815 : *
1816 : * @param additionalRecipients recipients parsed from the commit.
1817 : * @return set of CC strings to pass to {@code ReviewerModifier}.
1818 : */
1819 : ImmutableSet<String> getCombinedCcs(MailRecipients additionalRecipients) {
1820 88 : return getCombinedReviewers(cc, additionalRecipients.getCcOnly());
1821 : }
1822 :
1823 : private static ImmutableSet<String> getCombinedReviewers(
1824 : Set<String> strings, Set<Account.Id> ids) {
1825 88 : return Streams.concat(strings.stream(), ids.stream().map(Account.Id::toString))
1826 88 : .collect(toImmutableSet());
1827 : }
1828 :
1829 : void setWithholdComments(boolean withholdComments) {
1830 4 : this.withholdComments = withholdComments;
1831 4 : }
1832 :
1833 : boolean shouldPublishComments() {
1834 38 : if (withholdComments) {
1835 : // Validation messages of type WARNING have already been added, now withhold the comments.
1836 1 : return false;
1837 : }
1838 38 : if (publishComments) {
1839 4 : return true;
1840 : }
1841 37 : if (noPublishComments) {
1842 3 : return false;
1843 : }
1844 37 : return defaultPublishComments;
1845 : }
1846 :
1847 : /**
1848 : * returns the destination ref of the magic branch, and populates options in the cmdLineParser.
1849 : */
1850 : String parse(ListMultimap<String, String> pushOptions) throws CmdLineException {
1851 89 : String ref = RefNames.fullName(MagicBranch.getDestBranchName(cmd.getRefName()));
1852 :
1853 89 : ListMultimap<String, String> options = LinkedListMultimap.create(pushOptions);
1854 :
1855 : // Process and lop off the "%OPTION" suffix.
1856 89 : int optionStart = ref.indexOf('%');
1857 89 : if (0 < optionStart) {
1858 35 : for (String s : COMMAS.split(ref.substring(optionStart + 1))) {
1859 35 : int e = s.indexOf('=');
1860 35 : if (0 < e) {
1861 26 : options.put(s.substring(0, e), s.substring(e + 1));
1862 : } else {
1863 25 : options.put(s, "");
1864 : }
1865 35 : }
1866 35 : ref = ref.substring(0, optionStart);
1867 : }
1868 :
1869 89 : if (!options.isEmpty()) {
1870 35 : cmdLineParser.parseOptionMap(options);
1871 : }
1872 89 : return ref;
1873 : }
1874 :
1875 : public boolean shouldSetWorkInProgressOnNewChanges() {
1876 : // When wip or ready explicitly provided, leave it as is.
1877 88 : if (workInProgress) {
1878 13 : return true;
1879 : }
1880 88 : if (ready) {
1881 5 : return false;
1882 : }
1883 :
1884 88 : return projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
1885 88 : || firstNonNull(user.state().generalPreferences().workInProgressByDefault, false);
1886 : }
1887 :
1888 : NotifyResolver.Result getNotifyForNewChange() {
1889 88 : return NotifyResolver.Result.create(
1890 88 : firstNonNull(
1891 : notifyHandling,
1892 88 : shouldSetWorkInProgressOnNewChanges() ? NotifyHandling.OWNER : NotifyHandling.ALL),
1893 88 : ImmutableSetMultimap.<RecipientType, Account.Id>builder()
1894 88 : .putAll(RecipientType.TO, notifyTo)
1895 88 : .putAll(RecipientType.CC, notifyCc)
1896 88 : .putAll(RecipientType.BCC, notifyBcc)
1897 88 : .build());
1898 : }
1899 :
1900 : NotifyHandling getNotifyHandling(ChangeNotes notes) {
1901 37 : requireNonNull(notes);
1902 37 : if (notifyHandling != null) {
1903 1 : return notifyHandling;
1904 : }
1905 37 : if (workInProgress || (!ready && notes.getChange().isWorkInProgress())) {
1906 5 : return NotifyHandling.OWNER;
1907 : }
1908 37 : return NotifyHandling.ALL;
1909 : }
1910 : }
1911 :
1912 : /**
1913 : * Parse the magic branch data (refs/for/BRANCH/OPTIONALTOPIC%OPTIONS) into the magicBranch
1914 : * member.
1915 : *
1916 : * <p>Assumes we are handling a magic branch here.
1917 : */
1918 : private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException, IOException {
1919 89 : try (TraceTimer traceTimer = newTimer("parseMagicBranch")) {
1920 89 : logger.atFine().log("Found magic branch %s", cmd.getRefName());
1921 89 : MagicBranchInput magicBranch = new MagicBranchInput(user, projectState, cmd, labelTypes);
1922 :
1923 : String ref;
1924 89 : magicBranch.cmdLineParser = optionParserFactory.create(magicBranch);
1925 :
1926 : // Filter out plugin push options, as the parser would reject them as unknown.
1927 89 : ImmutableListMultimap<String, String> pushOptionsToParse =
1928 89 : pushOptions.entries().stream()
1929 89 : .filter(e -> !isPluginPushOption(e.getKey()))
1930 89 : .collect(toImmutableListMultimap(e -> e.getKey(), e -> e.getValue()));
1931 : try {
1932 89 : ref = magicBranch.parse(pushOptionsToParse);
1933 3 : } catch (CmdLineException e) {
1934 3 : if (!magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
1935 3 : logger.atFine().log("Invalid branch syntax");
1936 3 : reject(cmd, e.getMessage());
1937 3 : return;
1938 : }
1939 0 : ref = null; // never happens
1940 89 : }
1941 :
1942 89 : if (magicBranch.skipValidation) {
1943 3 : reject(
1944 : cmd,
1945 3 : String.format(
1946 : "\"--%s\" option is only supported for direct push", PUSH_OPTION_SKIP_VALIDATION));
1947 3 : return;
1948 : }
1949 :
1950 89 : if (magicBranch.topic != null && magicBranch.topic.length() > ChangeUtil.TOPIC_MAX_LENGTH) {
1951 3 : reject(
1952 3 : cmd, String.format("topic length exceeds the limit (%d)", ChangeUtil.TOPIC_MAX_LENGTH));
1953 : }
1954 :
1955 89 : if (magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
1956 3 : StringWriter w = new StringWriter();
1957 3 : w.write("\nHelp for refs/for/branch:\n\n");
1958 3 : magicBranch.cmdLineParser.printUsage(w, null);
1959 :
1960 3 : String pluginPushOptionsHelp =
1961 3 : StreamSupport.stream(pluginPushOptions.entries().spliterator(), /* parallel= */ false)
1962 3 : .map(
1963 : e ->
1964 3 : String.format(
1965 : "-o %s~%s: %s",
1966 3 : e.getPluginName(), e.get().getName(), e.get().getDescription()))
1967 3 : .sorted()
1968 3 : .collect(joining("\n"));
1969 3 : if (!pluginPushOptionsHelp.isEmpty()) {
1970 3 : w.write("\nPlugin push options:\n" + pluginPushOptionsHelp);
1971 : }
1972 :
1973 3 : addMessage(w.toString());
1974 3 : reject(cmd, "see help");
1975 3 : return;
1976 : }
1977 89 : if (projectState.isAllUsers() && RefNames.REFS_USERS_SELF.equals(ref)) {
1978 2 : logger.atFine().log("Handling %s", RefNames.REFS_USERS_SELF);
1979 2 : ref = RefNames.refsUsers(user.getAccountId());
1980 : }
1981 : // Pushing changes for review usually requires that the target branch exists, but there is an
1982 : // exception for the branch to which HEAD points to and for refs/meta/config. Pushing for
1983 : // review to these branches is allowed even if the branch does not exist yet. This allows to
1984 : // push initial code for review to an empty repository and to review an initial project
1985 : // configuration.
1986 89 : if (receivePackRefCache.exactRef(ref) == null
1987 19 : && !ref.equals(readHEAD(repo))
1988 5 : && !ref.equals(RefNames.REFS_CONFIG)) {
1989 5 : logger.atFine().log("Ref %s not found", ref);
1990 5 : if (ref.startsWith(Constants.R_HEADS)) {
1991 5 : String n = ref.substring(Constants.R_HEADS.length());
1992 5 : reject(cmd, "branch " + n + " not found");
1993 5 : } else {
1994 0 : reject(cmd, ref + " not found");
1995 : }
1996 5 : return;
1997 : }
1998 :
1999 89 : magicBranch.dest = BranchNameKey.create(project.getNameKey(), ref);
2000 89 : magicBranch.perm = permissions.ref(ref);
2001 :
2002 89 : Optional<AuthException> err =
2003 89 : checkRefPermission(magicBranch.perm, RefPermission.READ)
2004 89 : .map(Optional::of)
2005 89 : .orElse(checkRefPermission(magicBranch.perm, RefPermission.CREATE_CHANGE));
2006 89 : if (err.isPresent()) {
2007 1 : rejectProhibited(cmd, err.get());
2008 1 : return;
2009 : }
2010 :
2011 89 : if (magicBranch.isPrivate && magicBranch.removePrivate) {
2012 3 : reject(cmd, "the options 'private' and 'remove-private' are mutually exclusive");
2013 3 : return;
2014 : }
2015 :
2016 89 : boolean privateByDefault =
2017 : projectCache
2018 89 : .get(project.getNameKey())
2019 89 : .orElseThrow(illegalState(project.getNameKey()))
2020 89 : .is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
2021 89 : setChangeAsPrivate =
2022 : magicBranch.isPrivate || (privateByDefault && !magicBranch.removePrivate);
2023 :
2024 89 : if (receiveConfig.disablePrivateChanges && setChangeAsPrivate) {
2025 2 : reject(cmd, "private changes are disabled");
2026 2 : return;
2027 : }
2028 :
2029 89 : if (magicBranch.workInProgress && magicBranch.ready) {
2030 3 : reject(cmd, "the options 'wip' and 'ready' are mutually exclusive");
2031 3 : return;
2032 : }
2033 89 : if (magicBranch.publishComments && magicBranch.noPublishComments) {
2034 0 : reject(
2035 : cmd, "the options 'publish-comments' and 'no-publish-comments' are mutually exclusive");
2036 0 : return;
2037 : }
2038 :
2039 89 : if (magicBranch.submit) {
2040 9 : err = checkRefPermission(magicBranch.perm, RefPermission.UPDATE_BY_SUBMIT);
2041 9 : if (err.isPresent()) {
2042 3 : rejectProhibited(cmd, err.get());
2043 3 : return;
2044 : }
2045 : }
2046 :
2047 89 : RevWalk walk = receivePack.getRevWalk();
2048 : RevCommit tip;
2049 : try {
2050 89 : tip = walk.parseCommit(magicBranch.cmd.getNewId());
2051 89 : logger.atFine().log("Tip of push: %s", tip.name());
2052 0 : } catch (IOException ex) {
2053 0 : magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
2054 0 : logger.atSevere().withCause(ex).log(
2055 : "Invalid pack upload; one or more objects weren't sent");
2056 0 : return;
2057 89 : }
2058 :
2059 89 : String destBranch = magicBranch.dest.branch();
2060 : try {
2061 89 : if (magicBranch.merged) {
2062 3 : if (magicBranch.base != null) {
2063 0 : reject(cmd, "cannot use merged with base");
2064 0 : return;
2065 : }
2066 3 : Ref refTip = receivePackRefCache.exactRef(magicBranch.dest.branch());
2067 3 : if (refTip == null) {
2068 0 : reject(cmd, magicBranch.dest.branch() + " not found");
2069 0 : return;
2070 : }
2071 3 : RevCommit branchTip = receivePack.getRevWalk().parseCommit(refTip.getObjectId());
2072 3 : if (!walk.isMergedInto(tip, branchTip)) {
2073 3 : reject(cmd, "not merged into branch");
2074 3 : return;
2075 : }
2076 : }
2077 :
2078 : // If tip is a merge commit, or the root commit or
2079 : // if %base or %merged was specified, ignore newChangeForAllNotInTarget.
2080 89 : if (tip.getParentCount() > 1
2081 : || magicBranch.base != null
2082 : || magicBranch.merged
2083 89 : || tip.getParentCount() == 0) {
2084 28 : logger.atFine().log("Forcing newChangeForAllNotInTarget = false");
2085 28 : newChangeForAllNotInTarget = false;
2086 : }
2087 :
2088 89 : if (magicBranch.base != null) {
2089 3 : logger.atFine().log("Handling %%base: %s", magicBranch.base);
2090 3 : magicBranch.baseCommit = Lists.newArrayListWithCapacity(magicBranch.base.size());
2091 3 : for (ObjectId id : magicBranch.base) {
2092 : try {
2093 3 : magicBranch.baseCommit.add(walk.parseCommit(id));
2094 0 : } catch (IncorrectObjectTypeException notCommit) {
2095 0 : reject(cmd, "base must be a commit");
2096 0 : return;
2097 0 : } catch (MissingObjectException e) {
2098 0 : reject(cmd, "base not found");
2099 0 : return;
2100 0 : } catch (IOException e) {
2101 0 : throw new StorageException(
2102 0 : String.format("Project %s cannot read %s", project.getName(), id.name()), e);
2103 3 : }
2104 3 : }
2105 89 : } else if (newChangeForAllNotInTarget) {
2106 11 : Ref refTip = receivePackRefCache.exactRef(magicBranch.dest.branch());
2107 11 : if (refTip != null) {
2108 11 : RevCommit branchTip = receivePack.getRevWalk().parseCommit(refTip.getObjectId());
2109 11 : magicBranch.baseCommit = Collections.singletonList(branchTip);
2110 11 : logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
2111 11 : } else {
2112 : // The target branch does not exist. Usually pushing changes for review requires that
2113 : // the
2114 : // target branch exists, but there is an exception for the branch to which HEAD points
2115 : // to
2116 : // and for refs/meta/config. Pushing for review to these branches is allowed even if the
2117 : // branch does not exist yet. This allows to push initial code for review to an empty
2118 : // repository and to review an initial project configuration.
2119 3 : if (!ref.equals(readHEAD(repo)) && !ref.equals(RefNames.REFS_CONFIG)) {
2120 0 : reject(cmd, magicBranch.dest.branch() + " not found");
2121 0 : return;
2122 : }
2123 : }
2124 : }
2125 0 : } catch (IOException e) {
2126 0 : throw new StorageException(
2127 0 : String.format("Error walking to %s in project %s", destBranch, project.getName()), e);
2128 89 : }
2129 :
2130 89 : if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) {
2131 89 : this.magicBranch = magicBranch;
2132 89 : this.result.magicPush(true);
2133 : }
2134 8 : }
2135 89 : }
2136 :
2137 : private boolean isPluginPushOption(String pushOptionName) {
2138 4 : if (transitionalPluginOptions.contains(pushOptionName)) {
2139 3 : return true;
2140 : }
2141 4 : return StreamSupport.stream(pluginPushOptions.entries().spliterator(), /* parallel= */ false)
2142 4 : .anyMatch(e -> pushOptionName.equals(e.getPluginName() + "~" + e.get().getName()));
2143 : }
2144 :
2145 : // Validate that the new commits are connected with the target
2146 : // branch. If they aren't, we want to abort. We do this check by
2147 : // looking to see if we can compute a merge base between the new
2148 : // commits and the target branch head.
2149 : private boolean validateConnected(ReceiveCommand cmd, BranchNameKey dest, RevCommit tip) {
2150 89 : try (TraceTimer traceTimer =
2151 89 : newTimer("validateConnected", Metadata.builder().branchName(dest.branch()))) {
2152 89 : RevWalk walk = receivePack.getRevWalk();
2153 : try {
2154 89 : Ref targetRef = receivePackRefCache.exactRef(dest.branch());
2155 89 : if (targetRef == null || targetRef.getObjectId() == null) {
2156 : // The destination branch does not yet exist. Assume the
2157 : // history being sent for review will start it and thus
2158 : // is "connected" to the branch.
2159 16 : logger.atFine().log("Branch is unborn");
2160 :
2161 : // This is not an error condition.
2162 16 : return true;
2163 : }
2164 :
2165 89 : RevCommit h = walk.parseCommit(targetRef.getObjectId());
2166 89 : logger.atFine().log("Current branch tip: %s", h.name());
2167 89 : RevFilter oldRevFilter = walk.getRevFilter();
2168 : try {
2169 89 : walk.reset();
2170 89 : walk.setRevFilter(RevFilter.MERGE_BASE);
2171 89 : walk.markStart(tip);
2172 89 : walk.markStart(h);
2173 89 : if (walk.next() == null) {
2174 4 : reject(cmd, "no common ancestry");
2175 4 : return false;
2176 : }
2177 : } finally {
2178 89 : walk.reset();
2179 89 : walk.setRevFilter(oldRevFilter);
2180 : }
2181 0 : } catch (IOException e) {
2182 0 : cmd.setResult(REJECTED_MISSING_OBJECT);
2183 0 : logger.atSevere().withCause(e).log("Invalid pack upload; one or more objects weren't sent");
2184 0 : return false;
2185 89 : }
2186 89 : return true;
2187 17 : }
2188 : }
2189 :
2190 : private static String readHEAD(Repository repo) {
2191 : try {
2192 19 : String head = repo.getFullBranch();
2193 19 : logger.atFine().log("HEAD = %s", head);
2194 19 : return head;
2195 0 : } catch (IOException e) {
2196 0 : throw new StorageException("Cannot read HEAD symref", e);
2197 : }
2198 : }
2199 :
2200 : /**
2201 : * Update an existing change. If draft comments are to be published, these are validated and may
2202 : * be withheld.
2203 : *
2204 : * @return True if the command succeeded, false if it was rejected.
2205 : */
2206 : private boolean requestReplaceAndValidateComments(
2207 : ReceiveCommand cmd, boolean checkMergedInto, Change change, RevCommit newCommit)
2208 : throws IOException {
2209 38 : try (TraceTimer traceTimer = newTimer("requestReplaceAndValidateComments")) {
2210 38 : if (change.isClosed()) {
2211 3 : reject(
2212 : cmd,
2213 3 : changeFormatter.changeClosed(
2214 3 : ChangeReportFormatter.Input.builder().setChange(change).build()));
2215 3 : return false;
2216 : }
2217 :
2218 38 : ReplaceRequest req = new ReplaceRequest(change.getId(), newCommit, cmd, checkMergedInto);
2219 38 : if (replaceByChange.containsKey(req.ontoChange)) {
2220 0 : reject(cmd, "duplicate request");
2221 0 : return false;
2222 : }
2223 :
2224 38 : if (magicBranch != null && magicBranch.shouldPublishComments()) {
2225 4 : List<HumanComment> drafts =
2226 4 : commentsUtil.draftByChangeAuthor(
2227 4 : notesFactory.createChecked(change), user.getAccountId());
2228 4 : ImmutableList<CommentForValidation> draftsForValidation =
2229 4 : drafts.stream()
2230 4 : .map(
2231 : comment ->
2232 4 : CommentForValidation.create(
2233 : CommentSource.HUMAN,
2234 4 : comment.lineNbr > 0
2235 4 : ? CommentType.INLINE_COMMENT
2236 4 : : CommentType.FILE_COMMENT,
2237 : comment.message,
2238 4 : comment.message.length()))
2239 4 : .collect(toImmutableList());
2240 4 : CommentValidationContext ctx =
2241 4 : CommentValidationContext.create(
2242 4 : change.getChangeId(), change.getProject().get(), change.getDest().branch());
2243 4 : ImmutableList<CommentValidationFailure> commentValidationFailures =
2244 4 : PublishCommentUtil.findInvalidComments(ctx, commentValidators, draftsForValidation);
2245 4 : magicBranch.setWithholdComments(!commentValidationFailures.isEmpty());
2246 4 : commentValidationFailures.forEach(
2247 : failure ->
2248 1 : addMessage(
2249 1 : "Comment validation failure: " + failure.getMessage(),
2250 : ValidationMessage.Type.WARNING));
2251 : }
2252 :
2253 38 : replaceByChange.put(req.ontoChange, req);
2254 38 : return true;
2255 3 : }
2256 : }
2257 :
2258 : private void warnAboutMissingChangeId(ImmutableList<CreateRequest> newChanges) {
2259 89 : for (CreateRequest create : newChanges) {
2260 : try {
2261 88 : receivePack.getRevWalk().parseBody(create.commit);
2262 0 : } catch (IOException e) {
2263 0 : throw new StorageException("Can't parse commit", e);
2264 88 : }
2265 88 : List<String> idList = ChangeUtil.getChangeIdsFromFooter(create.commit, urlFormatter.get());
2266 :
2267 88 : if (idList.isEmpty()) {
2268 3 : messages.add(
2269 : new ValidationMessage("warning: pushing without Change-Id is deprecated", false));
2270 3 : break;
2271 : }
2272 88 : }
2273 89 : }
2274 :
2275 : private ImmutableList<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress)
2276 : throws IOException {
2277 89 : try (TraceTimer traceTimer = newTimer("selectNewAndReplacedChangesFromMagicBranch")) {
2278 89 : logger.atFine().log("Finding new and replaced changes");
2279 89 : List<CreateRequest> newChanges = new ArrayList<>();
2280 :
2281 89 : GroupCollector groupCollector =
2282 89 : GroupCollector.create(receivePackRefCache, psUtil, notesFactory, project.getNameKey());
2283 :
2284 89 : BranchCommitValidator validator =
2285 89 : commitValidatorFactory.create(projectState, magicBranch.dest, user);
2286 :
2287 : try {
2288 89 : RevCommit start = setUpWalkForSelectingChanges();
2289 89 : if (start == null) {
2290 0 : return ImmutableList.of();
2291 : }
2292 :
2293 89 : LinkedHashMap<RevCommit, ChangeLookup> pending = new LinkedHashMap<>();
2294 89 : Set<Change.Key> newChangeIds = new HashSet<>();
2295 89 : int maxBatchChanges = receiveConfig.getEffectiveMaxBatchChangesLimit(user);
2296 89 : int total = 0;
2297 89 : int alreadyTracked = 0;
2298 89 : boolean rejectImplicitMerges =
2299 89 : start.getParentCount() == 1
2300 : && projectCache
2301 89 : .get(project.getNameKey())
2302 89 : .orElseThrow(illegalState(project.getNameKey()))
2303 89 : .is(BooleanProjectConfig.REJECT_IMPLICIT_MERGES)
2304 : // Don't worry about implicit merges when creating changes for
2305 : // already-merged commits; they're already in history, so it's too
2306 : // late.
2307 : && !magicBranch.merged;
2308 : Set<RevCommit> mergedParents;
2309 89 : if (rejectImplicitMerges) {
2310 1 : mergedParents = new HashSet<>();
2311 : } else {
2312 89 : mergedParents = null;
2313 : }
2314 :
2315 : for (; ; ) {
2316 89 : RevCommit c = receivePack.getRevWalk().next();
2317 89 : if (c == null) {
2318 88 : break;
2319 : }
2320 89 : total++;
2321 89 : receivePack.getRevWalk().parseBody(c);
2322 89 : String name = c.name();
2323 89 : groupCollector.visit(c);
2324 89 : Collection<PatchSet.Id> existingPatchSets =
2325 89 : receivePackRefCache.patchSetIdsFromObjectId(c);
2326 :
2327 89 : if (rejectImplicitMerges) {
2328 1 : Collections.addAll(mergedParents, c.getParents());
2329 1 : mergedParents.remove(c);
2330 : }
2331 :
2332 89 : boolean commitAlreadyTracked = !existingPatchSets.isEmpty();
2333 89 : if (commitAlreadyTracked) {
2334 46 : alreadyTracked++;
2335 : // Corner cases where an existing commit might need a new group:
2336 : // A) Existing commit has a null group; wasn't assigned during schema
2337 : // upgrade, or schema upgrade is performed on a running server.
2338 : // B) Let A<-B<-C, then:
2339 : // 1. Push A to refs/heads/master
2340 : // 2. Push B to refs/for/master
2341 : // 3. Force push A~ to refs/heads/master
2342 : // 4. Push C to refs/for/master.
2343 : // B will be in existing so we aren't replacing the patch set. It
2344 : // used to have its own group, but now needs to to be changed to
2345 : // A's group.
2346 : // C) Commit is a PatchSet of a pre-existing change uploaded with a
2347 : // different target branch.
2348 46 : existingPatchSets.stream()
2349 46 : .forEach(i -> updateGroups.add(new UpdateGroupsRequest(i, c)));
2350 46 : if (!(newChangeForAllNotInTarget || magicBranch.base != null)) {
2351 46 : continue;
2352 : }
2353 : }
2354 :
2355 89 : List<String> idList = ChangeUtil.getChangeIdsFromFooter(c, urlFormatter.get());
2356 89 : if (!idList.isEmpty()) {
2357 89 : pending.put(c, lookupByChangeKey(c, Change.key(idList.get(idList.size() - 1).trim())));
2358 : } else {
2359 3 : pending.put(c, lookupByCommit(c));
2360 : }
2361 :
2362 89 : int n = pending.size() + newChanges.size();
2363 89 : if (maxBatchChanges != 0 && n > maxBatchChanges) {
2364 0 : logger.atFine().log("%d changes exceeds limit of %d", n, maxBatchChanges);
2365 0 : reject(
2366 : magicBranch.cmd,
2367 : "the number of pushed changes in a batch exceeds the max limit " + maxBatchChanges);
2368 0 : return ImmutableList.of();
2369 : }
2370 :
2371 89 : if (commitAlreadyTracked) {
2372 5 : boolean changeExistsOnDestBranch = false;
2373 5 : for (ChangeData cd : pending.get(c).destChanges) {
2374 3 : if (cd.change().getDest().equals(magicBranch.dest)) {
2375 3 : changeExistsOnDestBranch = true;
2376 3 : break;
2377 : }
2378 0 : }
2379 5 : if (changeExistsOnDestBranch) {
2380 3 : continue;
2381 : }
2382 :
2383 5 : logger.atFine().log(
2384 : "Creating new change for %s even though it is already tracked", name);
2385 : }
2386 :
2387 89 : BranchCommitValidator.Result validationResult =
2388 89 : validator.validateCommit(
2389 : repo,
2390 89 : receivePack.getRevWalk().getObjectReader(),
2391 : magicBranch.cmd,
2392 : c,
2393 89 : ImmutableListMultimap.copyOf(pushOptions),
2394 : magicBranch.merged,
2395 : rejectCommits,
2396 : null);
2397 89 : messages.addAll(validationResult.messages());
2398 89 : if (!validationResult.isValid()) {
2399 : // Not a change the user can propose? Abort as early as possible.
2400 6 : logger.atFine().log("Aborting early due to invalid commit");
2401 6 : return ImmutableList.of();
2402 : }
2403 :
2404 : // Don't allow merges to be uploaded in commit chain via all-not-in-target
2405 88 : if (newChangeForAllNotInTarget && c.getParentCount() > 1) {
2406 0 : reject(
2407 : magicBranch.cmd,
2408 : "Pushing merges in commit chains with 'all not in target' is not allowed,\n"
2409 : + "to override please set the base manually");
2410 0 : logger.atFine().log("Rejecting merge commit %s with newChangeForAllNotInTarget", name);
2411 : // TODO(dborowitz): Should we early return here?
2412 : }
2413 :
2414 88 : if (idList.isEmpty()) {
2415 3 : newChanges.add(new CreateRequest(c, magicBranch.dest.branch(), newProgress));
2416 3 : continue;
2417 : }
2418 88 : }
2419 88 : logger.atFine().log(
2420 : "Finished initial RevWalk with %d commits total: %d already"
2421 : + " tracked, %d new changes with no Change-Id, and %d deferred"
2422 : + " lookups",
2423 88 : total, alreadyTracked, newChanges.size(), pending.size());
2424 :
2425 88 : if (rejectImplicitMerges) {
2426 1 : rejectImplicitMerges(mergedParents);
2427 : }
2428 :
2429 88 : for (Iterator<ChangeLookup> itr = pending.values().iterator(); itr.hasNext(); ) {
2430 88 : ChangeLookup p = itr.next();
2431 88 : if (p.changeKey == null) {
2432 3 : continue;
2433 : }
2434 :
2435 88 : if (newChangeIds.contains(p.changeKey)) {
2436 3 : logger.atFine().log("Multiple commits with Change-Id %s", p.changeKey);
2437 3 : reject(magicBranch.cmd, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
2438 3 : return ImmutableList.of();
2439 : }
2440 :
2441 88 : List<ChangeData> changes = p.destChanges;
2442 88 : if (changes.size() > 1) {
2443 0 : logger.atFine().log(
2444 : "Multiple changes in branch %s with Change-Id %s: %s",
2445 : magicBranch.dest,
2446 : p.changeKey,
2447 0 : changes.stream().map(cd -> cd.getId().toString()).collect(joining()));
2448 : // WTF, multiple changes in this branch have the same key?
2449 : // Since the commit is new, the user should recreate it with
2450 : // a different Change-Id. In practice, we should never see
2451 : // this error message as Change-Id should be unique per branch.
2452 : //
2453 0 : reject(magicBranch.cmd, p.changeKey.get() + " has duplicates");
2454 0 : return ImmutableList.of();
2455 : }
2456 :
2457 88 : if (changes.size() == 1) {
2458 : // Schedule as a replacement to this one matching change.
2459 : //
2460 :
2461 38 : ObjectId currentPs = changes.get(0).currentPatchSet().commitId();
2462 : // If Commit is already current PatchSet of target Change.
2463 38 : if (p.commit.equals(currentPs)) {
2464 3 : if (pending.size() == 1) {
2465 : // There are no commits left to check, all commits in pending were already
2466 : // current PatchSet of the corresponding target changes.
2467 3 : reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
2468 : } else {
2469 : // Commit is already current PatchSet.
2470 : // Remove from pending and try next commit.
2471 3 : itr.remove();
2472 3 : continue;
2473 : }
2474 : }
2475 38 : if (requestReplaceAndValidateComments(
2476 38 : magicBranch.cmd, false, changes.get(0).change(), p.commit)) {
2477 38 : continue;
2478 : }
2479 3 : return ImmutableList.of();
2480 : }
2481 :
2482 88 : if (changes.isEmpty()) {
2483 88 : if (!isValidChangeId(p.changeKey.get())) {
2484 1 : reject(magicBranch.cmd, "invalid Change-Id");
2485 1 : return ImmutableList.of();
2486 : }
2487 :
2488 : // In case the change look up from the index failed,
2489 : // double check against the existing refs
2490 88 : if (foundInExistingPatchSets(receivePackRefCache.patchSetIdsFromObjectId(p.commit))) {
2491 3 : if (pending.size() == 1) {
2492 3 : reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
2493 3 : return ImmutableList.of();
2494 : }
2495 0 : itr.remove();
2496 0 : continue;
2497 : }
2498 88 : newChangeIds.add(p.changeKey);
2499 : }
2500 88 : newChanges.add(new CreateRequest(p.commit, magicBranch.dest.branch(), newProgress));
2501 88 : }
2502 88 : logger.atFine().log(
2503 : "Finished deferred lookups with %d updates and %d new changes",
2504 88 : replaceByChange.size(), newChanges.size());
2505 0 : } catch (IOException e) {
2506 : // Should never happen, the core receive process would have
2507 : // identified the missing object earlier before we got control.
2508 0 : throw new StorageException("Invalid pack upload; one or more objects weren't sent", e);
2509 88 : }
2510 :
2511 88 : if (newChanges.isEmpty() && replaceByChange.isEmpty()) {
2512 3 : reject(magicBranch.cmd, "no new changes");
2513 3 : return ImmutableList.of();
2514 : }
2515 88 : if (!newChanges.isEmpty() && magicBranch.edit) {
2516 0 : reject(magicBranch.cmd, "edit is not supported for new changes");
2517 0 : return ImmutableList.copyOf(newChanges);
2518 : }
2519 :
2520 88 : SortedSetMultimap<ObjectId, String> groups = groupCollector.getGroups();
2521 88 : List<Integer> newIds = seq.nextChangeIds(newChanges.size());
2522 88 : for (int i = 0; i < newChanges.size(); i++) {
2523 88 : CreateRequest create = newChanges.get(i);
2524 88 : create.setChangeId(newIds.get(i));
2525 88 : create.groups = ImmutableList.copyOf(groups.get(create.commit));
2526 : }
2527 88 : for (ReplaceRequest replace : replaceByChange.values()) {
2528 38 : replace.groups = ImmutableList.copyOf(groups.get(replace.newCommitId));
2529 38 : }
2530 88 : for (UpdateGroupsRequest update : updateGroups) {
2531 45 : update.groups = ImmutableList.copyOf(groups.get(update.commit));
2532 45 : }
2533 88 : logger.atFine().log("Finished updating groups from GroupCollector");
2534 88 : return ImmutableList.copyOf(newChanges);
2535 6 : }
2536 : }
2537 :
2538 : private boolean foundInExistingPatchSets(Collection<PatchSet.Id> existingPatchSets) {
2539 88 : try (TraceTimer traceTimer = newTimer("foundInExistingPatchSet")) {
2540 88 : for (PatchSet.Id psId : existingPatchSets) {
2541 5 : ChangeNotes notes = notesFactory.create(project.getNameKey(), psId.changeId());
2542 5 : Change change = notes.getChange();
2543 5 : if (change.getDest().equals(magicBranch.dest)) {
2544 3 : logger.atFine().log("Found change %s from existing refs.", change.getKey());
2545 : // Reindex the change asynchronously, ignoring errors.
2546 : @SuppressWarnings("unused")
2547 3 : Future<?> possiblyIgnoredError = indexer.indexAsync(project.getNameKey(), change.getId());
2548 3 : return true;
2549 : }
2550 5 : }
2551 88 : return false;
2552 3 : }
2553 : }
2554 :
2555 : private RevCommit setUpWalkForSelectingChanges() throws IOException {
2556 89 : try (TraceTimer traceTimer = newTimer("setUpWalkForSelectingChanges")) {
2557 89 : RevWalk rw = receivePack.getRevWalk();
2558 89 : RevCommit start = rw.parseCommit(magicBranch.cmd.getNewId());
2559 :
2560 89 : rw.reset();
2561 89 : rw.sort(RevSort.TOPO);
2562 89 : rw.sort(RevSort.REVERSE, true);
2563 89 : receivePack.getRevWalk().markStart(start);
2564 89 : if (magicBranch.baseCommit != null) {
2565 11 : markExplicitBasesUninteresting();
2566 89 : } else if (magicBranch.merged) {
2567 3 : logger.atFine().log("Marking parents of merged commit %s uninteresting", start.name());
2568 3 : for (RevCommit c : start.getParents()) {
2569 3 : rw.markUninteresting(c);
2570 : }
2571 : } else {
2572 89 : markHeadsAsUninteresting(rw, magicBranch.dest != null ? magicBranch.dest.branch() : null);
2573 : }
2574 89 : return start;
2575 : }
2576 : }
2577 :
2578 : private void markExplicitBasesUninteresting() throws IOException {
2579 11 : try (TraceTimer traceTimer = newTimer("markExplicitBasesUninteresting")) {
2580 11 : logger.atFine().log("Marking %d base commits uninteresting", magicBranch.baseCommit.size());
2581 11 : for (RevCommit c : magicBranch.baseCommit) {
2582 11 : receivePack.getRevWalk().markUninteresting(c);
2583 11 : }
2584 11 : Ref targetRef = receivePackRefCache.exactRef(magicBranch.dest.branch());
2585 11 : if (targetRef != null) {
2586 11 : logger.atFine().log(
2587 : "Marking target ref %s (%s) uninteresting",
2588 11 : magicBranch.dest.branch(), targetRef.getObjectId().name());
2589 11 : receivePack
2590 11 : .getRevWalk()
2591 11 : .markUninteresting(receivePack.getRevWalk().parseCommit(targetRef.getObjectId()));
2592 : }
2593 : }
2594 11 : }
2595 :
2596 : private void rejectImplicitMerges(Set<RevCommit> mergedParents) throws IOException {
2597 1 : try (TraceTimer traceTimer = newTimer("rejectImplicitMerges")) {
2598 1 : if (!mergedParents.isEmpty()) {
2599 1 : Ref targetRef = receivePackRefCache.exactRef(magicBranch.dest.branch());
2600 1 : if (targetRef != null) {
2601 1 : RevWalk rw = receivePack.getRevWalk();
2602 1 : RevCommit tip = rw.parseCommit(targetRef.getObjectId());
2603 1 : boolean containsImplicitMerges = true;
2604 1 : for (RevCommit p : mergedParents) {
2605 1 : containsImplicitMerges &= !rw.isMergedInto(p, tip);
2606 1 : }
2607 :
2608 1 : if (containsImplicitMerges) {
2609 1 : rw.reset();
2610 1 : for (RevCommit p : mergedParents) {
2611 1 : rw.markStart(p);
2612 1 : }
2613 1 : rw.markUninteresting(tip);
2614 : RevCommit c;
2615 1 : while ((c = rw.next()) != null) {
2616 1 : rw.parseBody(c);
2617 1 : messages.add(
2618 : new CommitValidationMessage(
2619 : "Implicit Merge of "
2620 1 : + abbreviateName(c, rw.getObjectReader())
2621 : + " "
2622 1 : + c.getShortMessage(),
2623 : ValidationMessage.Type.ERROR));
2624 : }
2625 1 : reject(magicBranch.cmd, "implicit merges detected");
2626 : }
2627 : }
2628 : }
2629 : }
2630 1 : }
2631 :
2632 : // Mark all branch tips as uninteresting in the given revwalk,
2633 : // so we get only the new commits when walking rw.
2634 : private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) throws IOException {
2635 96 : try (TraceTimer traceTimer =
2636 96 : newTimer("markHeadsAsUninteresting", Metadata.builder().branchName(forRef))) {
2637 96 : int i = 0;
2638 : for (Ref ref :
2639 96 : Iterables.concat(
2640 96 : receivePackRefCache.byPrefix(R_HEADS),
2641 96 : Collections.singletonList(receivePackRefCache.exactRef(forRef)))) {
2642 96 : if (ref != null && ref.getObjectId() != null) {
2643 : try {
2644 96 : rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
2645 96 : i++;
2646 1 : } catch (IOException e) {
2647 1 : logger.atWarning().withCause(e).log(
2648 1 : "Invalid ref %s in %s", ref.getName(), project.getName());
2649 96 : }
2650 : }
2651 96 : }
2652 96 : logger.atFine().log("Marked %d heads as uninteresting", i);
2653 : }
2654 96 : }
2655 :
2656 : private static boolean isValidChangeId(String idStr) {
2657 88 : return idStr.matches("^I[0-9a-fA-F]{40}$") && !idStr.matches("^I00*$");
2658 : }
2659 :
2660 : private static class ChangeLookup {
2661 : final RevCommit commit;
2662 :
2663 : @Nullable final Change.Key changeKey;
2664 : final List<ChangeData> destChanges;
2665 :
2666 89 : ChangeLookup(RevCommit c, @Nullable Change.Key key, final List<ChangeData> destChanges) {
2667 89 : this.commit = c;
2668 89 : this.changeKey = key;
2669 89 : this.destChanges = destChanges;
2670 89 : }
2671 : }
2672 :
2673 : private ChangeLookup lookupByChangeKey(RevCommit c, Change.Key key) {
2674 89 : try (TraceTimer traceTimer = newTimer("lookupByChangeKey")) {
2675 89 : List<ChangeData> byBranchKeyExactMatch =
2676 89 : queryProvider.get().byBranchKey(magicBranch.dest, key).stream()
2677 89 : .filter(cd -> cd.change().getKey().equals(key))
2678 89 : .collect(toList());
2679 89 : return new ChangeLookup(c, key, byBranchKeyExactMatch);
2680 : }
2681 : }
2682 :
2683 : private ChangeLookup lookupByCommit(RevCommit c) {
2684 3 : try (TraceTimer traceTimer = newTimer("lookupByCommit")) {
2685 3 : return new ChangeLookup(
2686 3 : c, null, queryProvider.get().byBranchCommit(magicBranch.dest, c.getName()));
2687 : }
2688 : }
2689 :
2690 : /** Represents a commit for which a Change should be created. */
2691 : private class CreateRequest {
2692 : final RevCommit commit;
2693 : final Task progress;
2694 : final String refName;
2695 :
2696 : Change.Id changeId;
2697 : ReceiveCommand cmd;
2698 : ChangeInserter ins;
2699 88 : List<String> groups = ImmutableList.of();
2700 :
2701 : Change change;
2702 :
2703 88 : CreateRequest(RevCommit commit, String refName, Task progress) {
2704 88 : this.commit = commit;
2705 88 : this.refName = refName;
2706 88 : this.progress = progress;
2707 88 : }
2708 :
2709 : private void setChangeId(int id) {
2710 88 : try (TraceTimer traceTimer = newTimer(CreateRequest.class, "setChangeId")) {
2711 88 : changeId = Change.id(id);
2712 88 : ins =
2713 : changeInserterFactory
2714 88 : .create(changeId, commit, refName)
2715 88 : .setTopic(magicBranch.topic)
2716 88 : .setPrivate(setChangeAsPrivate)
2717 88 : .setWorkInProgress(magicBranch.shouldSetWorkInProgressOnNewChanges())
2718 : // Changes already validated in validateNewCommits.
2719 88 : .setValidate(false);
2720 :
2721 88 : if (magicBranch.merged) {
2722 3 : ins.setStatus(Change.Status.MERGED);
2723 : }
2724 88 : cmd = new ReceiveCommand(ObjectId.zeroId(), commit, ins.getPatchSetId().toRefName());
2725 88 : if (receivePack.getPushCertificate() != null) {
2726 0 : ins.setPushCertificate(receivePack.getPushCertificate().toTextWithSignature());
2727 : }
2728 : }
2729 88 : }
2730 :
2731 : private void addOps(BatchUpdate bu) throws RestApiException {
2732 88 : try (TraceTimer traceTimer = newTimer(CreateRequest.class, "addOps")) {
2733 88 : checkState(changeId != null, "must call setChangeId before addOps");
2734 : try {
2735 88 : RevWalk rw = receivePack.getRevWalk();
2736 88 : rw.parseBody(commit);
2737 88 : final PatchSet.Id psId = ins.setGroups(groups).getPatchSetId();
2738 88 : Account.Id me = user.getAccountId();
2739 88 : List<FooterLine> footerLines = commit.getFooterLines();
2740 88 : requireNonNull(magicBranch);
2741 :
2742 : // TODO(dborowitz): Support reviewers by email from footers? Maybe not: kernel developers
2743 : // with AOSP accounts already complain about these notifications, and that would make it
2744 : // worse. Might be better to get rid of the feature entirely:
2745 : // https://groups.google.com/d/topic/repo-discuss/tIFxY7L4DXk/discussion
2746 88 : MailRecipients fromFooters = getRecipientsFromFooters(accountResolver, footerLines);
2747 88 : fromFooters.remove(me);
2748 :
2749 88 : Map<String, Short> approvals = magicBranch.labels;
2750 88 : StringBuilder msg =
2751 : new StringBuilder(
2752 88 : ApprovalsUtil.renderMessageWithApprovals(
2753 88 : psId.get(), approvals, Collections.emptyMap()));
2754 88 : msg.append('.');
2755 88 : if (!Strings.isNullOrEmpty(magicBranch.message)) {
2756 3 : msg.append("\n").append(magicBranch.message);
2757 : }
2758 :
2759 88 : bu.setNotify(magicBranch.getNotifyForNewChange());
2760 88 : bu.insertChange(
2761 88 : ins.setReviewersAndCcsAsStrings(
2762 88 : magicBranch.getCombinedReviewers(fromFooters),
2763 88 : magicBranch.getCombinedCcs(fromFooters))
2764 88 : .setApprovals(approvals)
2765 88 : .setMessage(msg.toString())
2766 88 : .setRequestScopePropagator(requestScopePropagator)
2767 88 : .setSendMail(true)
2768 88 : .setPatchSetDescription(magicBranch.message));
2769 88 : if (!magicBranch.hashtags.isEmpty()) {
2770 : // Any change owner is allowed to add hashtags when creating a change.
2771 3 : bu.addOp(
2772 : changeId,
2773 : hashtagsFactory
2774 3 : .create(new HashtagsInput(magicBranch.hashtags))
2775 3 : .setFireEvent(false));
2776 : }
2777 88 : if (!Strings.isNullOrEmpty(magicBranch.topic)) {
2778 22 : bu.addOp(changeId, setTopicFactory.create(magicBranch.topic));
2779 : }
2780 88 : if (magicBranch.ignoreAttentionSet) {
2781 3 : bu.addOp(changeId, new AttentionSetUnchangedOp());
2782 : }
2783 88 : bu.addOp(
2784 : changeId,
2785 88 : new BatchUpdateOp() {
2786 : @Override
2787 : public boolean updateChange(ChangeContext ctx) {
2788 88 : CreateRequest.this.change = ctx.getChange();
2789 88 : return false;
2790 : }
2791 : });
2792 88 : bu.addOp(changeId, new ChangeProgressOp(progress));
2793 0 : } catch (Exception e) {
2794 0 : throw asRestApiException(e);
2795 88 : }
2796 : }
2797 88 : }
2798 : }
2799 :
2800 : private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace)
2801 : throws RestApiException, UpdateException, IOException, ConfigInvalidException,
2802 : PermissionBackendException {
2803 8 : try (TraceTimer traceTimer = newTimer("submit")) {
2804 8 : Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size());
2805 8 : for (CreateRequest r : create) {
2806 7 : requireNonNull(
2807 : r.change,
2808 0 : () -> String.format("cannot submit new change %s; op may not have run", r.changeId));
2809 7 : bySha.put(r.commit, r.change);
2810 7 : }
2811 8 : for (ReplaceRequest r : replace) {
2812 3 : bySha.put(r.newCommitId, r.notes.getChange());
2813 3 : }
2814 8 : Change tipChange = bySha.get(magicBranch.cmd.getNewId());
2815 8 : requireNonNull(
2816 : tipChange,
2817 : () ->
2818 0 : String.format(
2819 : "tip of push does not correspond to a change; found these changes: %s", bySha));
2820 8 : logger.atFine().log(
2821 : "Processing submit with tip change %s (%s)",
2822 8 : tipChange.getId(), magicBranch.cmd.getNewId());
2823 8 : try (MergeOp op = mergeOpProvider.get()) {
2824 8 : SubmitInput submitInput = new SubmitInput();
2825 8 : submitInput.notify = magicBranch.notifyHandling;
2826 8 : submitInput.notifyDetails = new HashMap<>();
2827 8 : submitInput.notifyDetails.put(
2828 : RecipientType.TO,
2829 8 : new NotifyInfo(magicBranch.notifyTo.stream().map(Object::toString).collect(toList())));
2830 8 : submitInput.notifyDetails.put(
2831 : RecipientType.CC,
2832 8 : new NotifyInfo(magicBranch.notifyCc.stream().map(Object::toString).collect(toList())));
2833 8 : submitInput.notifyDetails.put(
2834 : RecipientType.BCC,
2835 8 : new NotifyInfo(magicBranch.notifyBcc.stream().map(Object::toString).collect(toList())));
2836 8 : op.merge(tipChange, user, false, submitInput, false);
2837 : }
2838 : }
2839 8 : }
2840 :
2841 : private void preparePatchSetsForReplace(ImmutableList<CreateRequest> newChanges) {
2842 89 : try (TraceTimer traceTimer =
2843 89 : newTimer(
2844 89 : "preparePatchSetsForReplace", Metadata.builder().resourceCount(newChanges.size()))) {
2845 : try {
2846 89 : readChangesForReplace();
2847 89 : for (ReplaceRequest req : replaceByChange.values()) {
2848 38 : if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
2849 38 : req.validateNewPatchSet();
2850 : }
2851 38 : }
2852 0 : } catch (IOException | PermissionBackendException e) {
2853 0 : throw new StorageException(
2854 0 : "Cannot read repository before replacement for project " + project.getName(), e);
2855 89 : }
2856 89 : logger.atFine().log("Read %d changes to replace", replaceByChange.size());
2857 :
2858 89 : if (magicBranch != null && magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
2859 : // Cancel creations tied to refs/for/ command.
2860 9 : for (ReplaceRequest req : replaceByChange.values()) {
2861 5 : if (req.inputCommand == magicBranch.cmd && req.cmd != null) {
2862 0 : req.cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, "aborted");
2863 : }
2864 5 : }
2865 9 : for (CreateRequest req : newChanges) {
2866 1 : req.cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, "aborted");
2867 1 : }
2868 : }
2869 : }
2870 89 : }
2871 :
2872 : private void readChangesForReplace() {
2873 89 : try (TraceTimer traceTimer = newTimer("readChangesForReplace")) {
2874 89 : replaceByChange.values().stream()
2875 89 : .map(r -> r.ontoChange)
2876 89 : .map(id -> notesFactory.create(repo, project.getNameKey(), id))
2877 89 : .forEach(notes -> replaceByChange.get(notes.getChangeId()).notes = notes);
2878 : }
2879 89 : }
2880 :
2881 : /** Represents a commit that should be stored in a new patchset of an existing change. */
2882 : private class ReplaceRequest {
2883 : final Change.Id ontoChange;
2884 : final ObjectId newCommitId;
2885 : final ReceiveCommand inputCommand;
2886 : final boolean checkMergedInto;
2887 : RevCommit revCommit;
2888 : ChangeNotes notes;
2889 : BiMap<RevCommit, PatchSet.Id> revisions;
2890 : PatchSet.Id psId;
2891 : ReceiveCommand prev;
2892 : ReceiveCommand cmd;
2893 : PatchSetInfo info;
2894 : PatchSet.Id priorPatchSet;
2895 38 : List<String> groups = ImmutableList.of();
2896 : ReplaceOp replaceOp;
2897 :
2898 : ReplaceRequest(
2899 : Change.Id toChange, RevCommit newCommit, ReceiveCommand cmd, boolean checkMergedInto)
2900 38 : throws IOException {
2901 38 : this.ontoChange = toChange;
2902 38 : this.newCommitId = newCommit.copy();
2903 38 : this.inputCommand = requireNonNull(cmd);
2904 38 : this.checkMergedInto = checkMergedInto;
2905 :
2906 : try {
2907 38 : revCommit = receivePack.getRevWalk().parseCommit(newCommitId);
2908 0 : } catch (IOException e) {
2909 0 : revCommit = null;
2910 38 : }
2911 38 : revisions = HashBiMap.create();
2912 38 : for (Ref ref : receivePackRefCache.byPrefix(RefNames.changeRefPrefix(toChange))) {
2913 : try {
2914 38 : PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
2915 38 : if (psId != null) {
2916 38 : revisions.forcePut(receivePack.getRevWalk().parseCommit(ref.getObjectId()), psId);
2917 : }
2918 0 : } catch (IOException err) {
2919 0 : logger.atWarning().withCause(err).log(
2920 0 : "Project %s contains invalid change ref %s", project.getName(), ref.getName());
2921 38 : }
2922 38 : }
2923 38 : }
2924 :
2925 : /**
2926 : * Validate the new patch set commit for this change.
2927 : *
2928 : * <p><strong>Side effects:</strong>
2929 : *
2930 : * <ul>
2931 : * <li>May add error or warning messages to the progress monitor
2932 : * <li>Will reject {@code cmd} prior to returning false
2933 : * <li>May reset {@code receivePack.getRevWalk()}; do not call in the middle of a walk.
2934 : * </ul>
2935 : *
2936 : * @return whether the new commit is valid
2937 : */
2938 : boolean validateNewPatchSet() throws IOException, PermissionBackendException {
2939 38 : try (TraceTimer traceTimer = newTimer("validateNewPatchSet")) {
2940 38 : if (!validateNewPatchSetNoteDb()) {
2941 5 : return false;
2942 : }
2943 37 : sameTreeWarning();
2944 :
2945 37 : if (magicBranch != null) {
2946 37 : validateMagicBranchWipStatusChange();
2947 37 : if (inputCommand.getResult() != NOT_ATTEMPTED) {
2948 3 : return false;
2949 : }
2950 :
2951 37 : if (magicBranch.edit) {
2952 3 : return newEdit();
2953 : }
2954 : }
2955 :
2956 37 : newPatchSet();
2957 37 : return true;
2958 5 : }
2959 : }
2960 :
2961 : boolean validateNewPatchSetForAutoClose() throws IOException, PermissionBackendException {
2962 6 : if (!validateNewPatchSetNoteDb()) {
2963 0 : return false;
2964 : }
2965 :
2966 6 : newPatchSet();
2967 6 : return true;
2968 : }
2969 :
2970 : /** Validates the new PS against permissions and notedb status. */
2971 : private boolean validateNewPatchSetNoteDb() throws IOException, PermissionBackendException {
2972 38 : try (TraceTimer traceTimer = newTimer("validateNewPatchSetNoteDb")) {
2973 38 : if (notes == null) {
2974 0 : reject(inputCommand, "change " + ontoChange + " not found");
2975 0 : return false;
2976 : }
2977 :
2978 38 : Change change = notes.getChange();
2979 38 : priorPatchSet = change.currentPatchSetId();
2980 38 : if (!revisions.containsValue(priorPatchSet)) {
2981 0 : metrics.psRevisionMissing.increment();
2982 0 : logger.atWarning().log(
2983 : "Change %d is missing revision for patch set %s"
2984 : + " (it has revisions for these patch sets: %s)",
2985 0 : change.getChangeId(),
2986 0 : priorPatchSet.getId(),
2987 0 : Iterables.toString(
2988 0 : revisions.values().stream()
2989 0 : .limit(100) // Enough for "normal" changes.
2990 0 : .map(PatchSet.Id::getId)
2991 0 : .collect(Collectors.toList())));
2992 0 : reject(inputCommand, "change " + ontoChange + " missing revisions");
2993 0 : return false;
2994 : }
2995 :
2996 38 : RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
2997 :
2998 : // Not allowed to create a new patch set if the current patch set is locked.
2999 38 : if (psUtil.isPatchSetLocked(notes)) {
3000 3 : reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
3001 3 : return false;
3002 : }
3003 :
3004 38 : if (!permissions.change(notes).test(ChangePermission.ADD_PATCH_SET)) {
3005 2 : reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
3006 2 : return false;
3007 : }
3008 :
3009 37 : if (change.isClosed()) {
3010 0 : reject(inputCommand, "change " + ontoChange + " closed");
3011 0 : return false;
3012 37 : } else if (revisions.containsKey(newCommit)) {
3013 0 : reject(inputCommand, "commit already exists (in the change)");
3014 0 : return false;
3015 : }
3016 :
3017 37 : List<PatchSet.Id> existingPatchSetsWithSameCommit =
3018 37 : receivePackRefCache.patchSetIdsFromObjectId(newCommit);
3019 37 : if (!existingPatchSetsWithSameCommit.isEmpty()) {
3020 : // TODO(hiesel, hanwen): Remove this check entirely when Gerrit requires change IDs
3021 : // without the option to turn that off.
3022 0 : reject(
3023 : inputCommand,
3024 : "commit already exists (in the project): "
3025 0 : + existingPatchSetsWithSameCommit.get(0).toRefName());
3026 0 : return false;
3027 : }
3028 :
3029 37 : try (TraceTimer traceTimer2 = newTimer("validateNewPatchSetNoteDb#isMergedInto")) {
3030 37 : for (RevCommit prior : revisions.keySet()) {
3031 : // Don't allow a change to directly depend upon itself. This is a
3032 : // very common error due to users making a new commit rather than
3033 : // amending when trying to address review comments.
3034 37 : if (receivePack.getRevWalk().isMergedInto(prior, newCommit)) {
3035 3 : reject(inputCommand, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
3036 3 : return false;
3037 : }
3038 37 : }
3039 3 : }
3040 :
3041 37 : return true;
3042 5 : }
3043 : }
3044 :
3045 : /** Validates whether the WIP change is allowed. Rejects inputCommand if not. */
3046 : private void validateMagicBranchWipStatusChange() throws PermissionBackendException {
3047 37 : Change change = notes.getChange();
3048 37 : if ((magicBranch.workInProgress || magicBranch.ready)
3049 5 : && magicBranch.workInProgress != change.isWorkInProgress()
3050 5 : && !user.getAccountId().equals(change.getOwner())) {
3051 3 : if (!permissions.test(ProjectPermission.WRITE_CONFIG)) {
3052 3 : if (!permissions.change(notes).test(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE)) {
3053 3 : reject(inputCommand, ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP);
3054 : }
3055 : }
3056 : }
3057 37 : }
3058 :
3059 : /** prints a warning if the new PS has the same tree as the previous commit. */
3060 : private void sameTreeWarning() throws IOException {
3061 37 : try (TraceTimer traceTimer = newTimer("sameTreeWarning")) {
3062 37 : RevWalk rw = receivePack.getRevWalk();
3063 37 : RevCommit newCommit = rw.parseCommit(newCommitId);
3064 37 : RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
3065 :
3066 37 : if (newCommit.getTree().equals(priorCommit.getTree())) {
3067 11 : rw.parseBody(newCommit);
3068 11 : rw.parseBody(priorCommit);
3069 11 : boolean messageEq =
3070 11 : Objects.equals(newCommit.getFullMessage(), priorCommit.getFullMessage());
3071 11 : boolean parentsEq = parentsEqual(newCommit, priorCommit);
3072 11 : boolean authorEq = authorEqual(newCommit, priorCommit);
3073 11 : ObjectReader reader = receivePack.getRevWalk().getObjectReader();
3074 :
3075 11 : if (messageEq && parentsEq && authorEq) {
3076 8 : addMessage(
3077 8 : String.format(
3078 : "warning: no changes between prior commit %s and new commit %s",
3079 8 : abbreviateName(priorCommit, reader), abbreviateName(newCommit, reader)));
3080 : } else {
3081 7 : StringBuilder msg = new StringBuilder();
3082 7 : msg.append("warning: ").append(abbreviateName(newCommit, reader));
3083 7 : msg.append(":");
3084 7 : msg.append(" no files changed");
3085 7 : if (!authorEq) {
3086 3 : msg.append(", author changed");
3087 : }
3088 7 : if (!messageEq) {
3089 6 : msg.append(", message updated");
3090 : }
3091 7 : if (!parentsEq) {
3092 5 : msg.append(", was rebased");
3093 : }
3094 7 : addMessage(msg.toString());
3095 : }
3096 : }
3097 : }
3098 37 : }
3099 :
3100 : /**
3101 : * Sets cmd and prev to the ReceiveCommands for change edits. Returns false if there was a
3102 : * failure.
3103 : */
3104 : private boolean newEdit() {
3105 3 : try (TraceTimer traceTimer = newTimer("newEdit")) {
3106 3 : psId = notes.getChange().currentPatchSetId();
3107 : Optional<ChangeEdit> edit;
3108 :
3109 : try {
3110 3 : edit = editUtil.byChange(notes, user);
3111 0 : } catch (AuthException | IOException e) {
3112 0 : logger.atSevere().withCause(e).log("Cannot retrieve edit");
3113 0 : return false;
3114 3 : }
3115 :
3116 3 : if (edit.isPresent()) {
3117 0 : if (edit.get().getBasePatchSet().id().equals(psId)) {
3118 : // replace edit
3119 0 : cmd =
3120 : new ReceiveCommand(
3121 0 : edit.get().getEditCommit(), newCommitId, edit.get().getRefName());
3122 : } else {
3123 : // delete old edit ref on rebase
3124 0 : prev =
3125 : new ReceiveCommand(
3126 0 : edit.get().getEditCommit(), ObjectId.zeroId(), edit.get().getRefName());
3127 0 : createEditCommand();
3128 : }
3129 : } else {
3130 3 : createEditCommand();
3131 : }
3132 :
3133 3 : return true;
3134 0 : }
3135 : }
3136 :
3137 : /** Creates a ReceiveCommand for a new edit. */
3138 : private void createEditCommand() {
3139 3 : cmd =
3140 : new ReceiveCommand(
3141 3 : ObjectId.zeroId(),
3142 : newCommitId,
3143 3 : RefNames.refsEdit(user.getAccountId(), notes.getChangeId(), psId));
3144 3 : }
3145 :
3146 : /** Updates 'this' to add a new patchset. */
3147 : private void newPatchSet() throws IOException {
3148 37 : try (TraceTimer traceTimer = newTimer("newPatchSet")) {
3149 37 : RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
3150 37 : psId = nextPatchSetId(notes.getChange().currentPatchSetId());
3151 37 : info = patchSetInfoFactory.get(receivePack.getRevWalk(), newCommit, psId);
3152 37 : cmd = new ReceiveCommand(ObjectId.zeroId(), newCommitId, psId.toRefName());
3153 : }
3154 37 : }
3155 :
3156 : private PatchSet.Id nextPatchSetId(PatchSet.Id psId) throws IOException {
3157 37 : PatchSet.Id next = ChangeUtil.nextPatchSetId(psId);
3158 37 : while (receivePackRefCache.exactRef(next.toRefName()) != null) {
3159 0 : next = ChangeUtil.nextPatchSetId(next);
3160 : }
3161 37 : return next;
3162 : }
3163 :
3164 : void addOps(BatchUpdate bu, @Nullable Task progress) throws IOException {
3165 37 : try (TraceTimer traceTimer = newTimer("addOps")) {
3166 37 : if (magicBranch != null && magicBranch.edit) {
3167 3 : bu.addOp(notes.getChangeId(), new ReindexOnlyOp());
3168 3 : if (prev != null) {
3169 0 : bu.addRepoOnlyOp(new UpdateOneRefOp(prev));
3170 : }
3171 3 : bu.addRepoOnlyOp(new UpdateOneRefOp(cmd));
3172 3 : return;
3173 : }
3174 37 : RevWalk rw = receivePack.getRevWalk();
3175 : // TODO(dborowitz): Move to ReplaceOp#updateRepo.
3176 37 : RevCommit newCommit = rw.parseCommit(newCommitId);
3177 37 : rw.parseBody(newCommit);
3178 :
3179 37 : RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
3180 37 : replaceOp =
3181 37 : replaceOpFactory.create(
3182 : projectState,
3183 37 : notes.getChange(),
3184 : checkMergedInto,
3185 37 : checkMergedInto ? inputCommand.getNewId().name() : null,
3186 : priorPatchSet,
3187 : priorCommit,
3188 : psId,
3189 : newCommit,
3190 : info,
3191 : groups,
3192 : magicBranch,
3193 37 : receivePack.getPushCertificate(),
3194 : requestScopePropagator);
3195 37 : bu.addOp(notes.getChangeId(), replaceOp);
3196 37 : if (progress != null) {
3197 37 : bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress));
3198 : }
3199 37 : bu.addRepoOnlyOp(
3200 37 : new RepoOnlyOp() {
3201 : @Override
3202 : public void updateRepo(RepoContext ctx) throws Exception {
3203 : // Create auto merge ref if the new patch set is a merge commit. This is only
3204 : // required for new patch sets on existing changes as these do not go through
3205 : // PatchSetInserter. New changes pushed via git go through ChangeInserter and have
3206 : // their auto merge commits created there.
3207 37 : Optional<ReceiveCommand> autoMerge =
3208 37 : autoMerger.createAutoMergeCommitIfNecessary(
3209 37 : ctx.getRepoView(),
3210 37 : ctx.getRevWalk(),
3211 37 : ctx.getInserter(),
3212 37 : ctx.getRevWalk().parseCommit(newCommitId));
3213 37 : if (autoMerge.isPresent()) {
3214 5 : ctx.addRefUpdate(autoMerge.get());
3215 : }
3216 37 : }
3217 : });
3218 3 : }
3219 37 : }
3220 :
3221 : @Nullable
3222 : String getRejectMessage() {
3223 37 : return replaceOp != null ? replaceOp.getRejectMessage() : null;
3224 : }
3225 :
3226 : Optional<String> getOutdatedApprovalsMessage() {
3227 37 : return replaceOp != null ? replaceOp.getOutdatedApprovalsMessage() : Optional.empty();
3228 : }
3229 : }
3230 :
3231 : private class UpdateGroupsRequest {
3232 : final PatchSet.Id psId;
3233 : final RevCommit commit;
3234 46 : List<String> groups = ImmutableList.of();
3235 :
3236 46 : UpdateGroupsRequest(PatchSet.Id psId, RevCommit commit) {
3237 46 : this.psId = psId;
3238 46 : this.commit = commit;
3239 46 : }
3240 :
3241 : private void addOps(BatchUpdate bu) {
3242 45 : bu.addOp(
3243 45 : psId.changeId(),
3244 45 : new BatchUpdateOp() {
3245 : @Override
3246 : public boolean updateChange(ChangeContext ctx) {
3247 45 : PatchSet ps = psUtil.get(ctx.getNotes(), psId);
3248 45 : List<String> oldGroups = ps.groups();
3249 45 : if (oldGroups == null) {
3250 0 : if (groups == null) {
3251 0 : return false;
3252 : }
3253 45 : } else if (sameGroups(oldGroups, groups)) {
3254 45 : return false;
3255 : }
3256 0 : ctx.getUpdate(psId).setGroups(groups);
3257 0 : return true;
3258 : }
3259 : });
3260 45 : }
3261 :
3262 : private boolean sameGroups(List<String> a, List<String> b) {
3263 45 : return Sets.newHashSet(a).equals(Sets.newHashSet(b));
3264 : }
3265 : }
3266 :
3267 : private class UpdateOneRefOp implements RepoOnlyOp {
3268 : final ReceiveCommand cmd;
3269 :
3270 47 : private UpdateOneRefOp(ReceiveCommand cmd) {
3271 47 : this.cmd = requireNonNull(cmd);
3272 47 : }
3273 :
3274 : @Override
3275 : public void updateRepo(RepoContext ctx) throws IOException {
3276 47 : ctx.addRefUpdate(cmd);
3277 47 : }
3278 :
3279 : @Override
3280 : public void postUpdate(PostUpdateContext ctx) {
3281 47 : String refName = cmd.getRefName();
3282 47 : if (cmd.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
3283 42 : logger.atFine().log("Updating tag cache on fast-forward of %s", cmd.getRefName());
3284 42 : tagCache.updateFastForward(project.getNameKey(), refName, cmd.getOldId(), cmd.getNewId());
3285 : }
3286 47 : if (isConfig(cmd)) {
3287 5 : logger.atFine().log("Reloading project in cache");
3288 5 : projectCache.evictAndReindex(project);
3289 5 : ProjectState ps =
3290 5 : projectCache.get(project.getNameKey()).orElseThrow(illegalState(project.getNameKey()));
3291 : try {
3292 5 : logger.atFine().log("Updating project description");
3293 5 : repo.setGitwebDescription(ps.getProject().getDescription());
3294 0 : } catch (IOException e) {
3295 0 : throw new StorageException("cannot update description of " + project.getName(), e);
3296 5 : }
3297 5 : if (allProjectsName.equals(project.getNameKey())) {
3298 : try {
3299 1 : createGroupPermissionSyncer.syncIfNeeded();
3300 0 : } catch (IOException | ConfigInvalidException e) {
3301 0 : throw new StorageException("cannot update description of " + project.getName(), e);
3302 1 : }
3303 : }
3304 : }
3305 47 : }
3306 : }
3307 :
3308 : private static class ReindexOnlyOp implements BatchUpdateOp {
3309 : @Override
3310 : public boolean updateChange(ChangeContext ctx) {
3311 : // Trigger reindexing even though change isn't actually updated.
3312 3 : return true;
3313 : }
3314 : }
3315 :
3316 : private static boolean parentsEqual(RevCommit a, RevCommit b) {
3317 11 : if (a.getParentCount() != b.getParentCount()) {
3318 4 : return false;
3319 : }
3320 10 : for (int i = 0; i < a.getParentCount(); i++) {
3321 10 : if (!a.getParent(i).equals(b.getParent(i))) {
3322 4 : return false;
3323 : }
3324 : }
3325 10 : return true;
3326 : }
3327 :
3328 : private static boolean authorEqual(RevCommit a, RevCommit b) {
3329 11 : PersonIdent aAuthor = a.getAuthorIdent();
3330 11 : PersonIdent bAuthor = b.getAuthorIdent();
3331 :
3332 11 : if (aAuthor == null && bAuthor == null) {
3333 0 : return true;
3334 11 : } else if (aAuthor == null || bAuthor == null) {
3335 0 : return false;
3336 : }
3337 :
3338 11 : return Objects.equals(aAuthor.getName(), bAuthor.getName())
3339 11 : && Objects.equals(aAuthor.getEmailAddress(), bAuthor.getEmailAddress());
3340 : }
3341 :
3342 : // Run RefValidators on the command. If any validator fails, the command status is set to
3343 : // REJECTED, and the return value is 'false'
3344 : private boolean validRefOperation(ReceiveCommand cmd) {
3345 49 : try (TraceTimer traceTimer = newTimer("validRefOperation")) {
3346 49 : RefOperationValidators refValidators =
3347 49 : refValidatorsFactory.create(
3348 49 : getProject(), user, cmd, ImmutableListMultimap.copyOf(pushOptions));
3349 :
3350 : try {
3351 49 : messages.addAll(refValidators.validateForRefOperation());
3352 5 : } catch (RefOperationValidationException e) {
3353 5 : messages.addAll(e.getMessages());
3354 5 : reject(cmd, e.getMessage());
3355 5 : return false;
3356 49 : }
3357 :
3358 49 : return true;
3359 5 : }
3360 : }
3361 :
3362 : /**
3363 : * Validates the commits that a regular push brings in.
3364 : *
3365 : * <p>On validation failure, the command is rejected.
3366 : */
3367 : private void validateRegularPushCommits(BranchNameKey branch, ReceiveCommand cmd)
3368 : throws PermissionBackendException {
3369 49 : try (TraceTimer traceTimer =
3370 49 : newTimer("validateRegularPushCommits", Metadata.builder().branchName(branch.branch()))) {
3371 49 : boolean skipValidation =
3372 49 : !RefNames.REFS_CONFIG.equals(cmd.getRefName())
3373 47 : && !(MagicBranch.isMagicBranch(cmd.getRefName())
3374 47 : || NEW_PATCHSET_PATTERN.matcher(cmd.getRefName()).matches())
3375 49 : && pushOptions.containsKey(PUSH_OPTION_SKIP_VALIDATION);
3376 49 : if (skipValidation) {
3377 4 : if (projectState.is(BooleanProjectConfig.USE_SIGNED_OFF_BY)) {
3378 0 : reject(cmd, "requireSignedOffBy prevents option " + PUSH_OPTION_SKIP_VALIDATION);
3379 0 : return;
3380 : }
3381 :
3382 4 : Optional<AuthException> err =
3383 4 : checkRefPermission(permissions.ref(branch.branch()), RefPermission.SKIP_VALIDATION);
3384 4 : if (err.isPresent()) {
3385 4 : rejectProhibited(cmd, err.get());
3386 4 : return;
3387 : }
3388 3 : if (!Iterables.isEmpty(rejectCommits)) {
3389 0 : reject(cmd, "reject-commits prevents " + PUSH_OPTION_SKIP_VALIDATION);
3390 : }
3391 : }
3392 :
3393 49 : BranchCommitValidator validator = commitValidatorFactory.create(projectState, branch, user);
3394 49 : RevWalk walk = receivePack.getRevWalk();
3395 49 : walk.reset();
3396 49 : walk.sort(RevSort.NONE);
3397 : try {
3398 49 : RevObject parsedObject = walk.parseAny(cmd.getNewId());
3399 49 : if (!(parsedObject instanceof RevCommit)) {
3400 2 : return;
3401 : }
3402 47 : walk.markStart((RevCommit) parsedObject);
3403 47 : markHeadsAsUninteresting(walk, cmd.getRefName());
3404 47 : int limit = receiveConfig.maxBatchCommits;
3405 47 : int n = 0;
3406 47 : for (RevCommit c; (c = walk.next()) != null; ) {
3407 : // Even if skipValidation is set, we still get here when at least one plugin
3408 : // commit validator requires to validate all commits. In this case, however,
3409 : // we don't need to check the commit limit.
3410 47 : if (++n > limit && !skipValidation) {
3411 3 : logger.atFine().log("Number of new commits exceeds limit of %d", limit);
3412 3 : reject(
3413 : cmd,
3414 3 : String.format(
3415 3 : "more than %d commits, and %s not set", limit, PUSH_OPTION_SKIP_VALIDATION));
3416 3 : return;
3417 : }
3418 47 : if (!receivePackRefCache.patchSetIdsFromObjectId(c).isEmpty()) {
3419 11 : continue;
3420 : }
3421 :
3422 47 : BranchCommitValidator.Result validationResult =
3423 47 : validator.validateCommit(
3424 : repo,
3425 47 : walk.getObjectReader(),
3426 : cmd,
3427 : c,
3428 47 : ImmutableListMultimap.copyOf(pushOptions),
3429 : false,
3430 : rejectCommits,
3431 : null,
3432 : skipValidation);
3433 47 : messages.addAll(validationResult.messages());
3434 47 : if (!validationResult.isValid()) {
3435 8 : break;
3436 : }
3437 45 : }
3438 47 : logger.atFine().log("Validated %d new commits", n);
3439 0 : } catch (IOException err) {
3440 0 : cmd.setResult(REJECTED_MISSING_OBJECT);
3441 0 : logger.atSevere().withCause(err).log(
3442 : "Invalid pack upload; one or more objects weren't sent");
3443 47 : }
3444 6 : }
3445 47 : }
3446 :
3447 : private void autoCloseChanges(ReceiveCommand cmd, Task progress) {
3448 38 : try (TraceTimer traceTimer = newTimer("autoCloseChanges")) {
3449 38 : logger.atFine().log("Starting auto-closing of changes");
3450 38 : String refName = cmd.getRefName();
3451 38 : Set<Change.Id> ids = new HashSet<>();
3452 :
3453 : // TODO(dborowitz): Combine this BatchUpdate with the main one in
3454 : // handleRegularCommands
3455 : try {
3456 38 : retryHelper
3457 38 : .changeUpdate(
3458 : "autoCloseChanges",
3459 : updateFactory -> {
3460 38 : try (BatchUpdate bu =
3461 38 : updateFactory.create(projectState.getNameKey(), user, TimeUtil.now());
3462 38 : ObjectInserter ins = repo.newObjectInserter();
3463 38 : ObjectReader reader = ins.newReader();
3464 38 : RevWalk rw = new RevWalk(reader)) {
3465 38 : if (ObjectId.zeroId().equals(cmd.getOldId())) {
3466 : // The user is creating a new branch. The branch can't contain any changes, so
3467 : // auto-closing doesn't apply. Exiting here early to spare any further,
3468 : // potentially expensive computation that loop over all commits.
3469 22 : return null;
3470 : }
3471 :
3472 38 : bu.setRepository(repo, rw, ins);
3473 : // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
3474 :
3475 38 : RevCommit newTip = rw.parseCommit(cmd.getNewId());
3476 38 : BranchNameKey branch = BranchNameKey.create(project.getNameKey(), refName);
3477 :
3478 38 : rw.reset();
3479 38 : rw.sort(RevSort.REVERSE);
3480 38 : rw.markStart(newTip);
3481 38 : rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
3482 :
3483 38 : Map<Change.Key, ChangeData> changeDataByKey = null;
3484 38 : List<ReplaceRequest> replaceAndClose = new ArrayList<>();
3485 :
3486 38 : int existingPatchSets = 0;
3487 38 : int newPatchSets = 0;
3488 38 : SubmissionId submissionId = null;
3489 : COMMIT:
3490 38 : for (RevCommit c; (c = rw.next()) != null; ) {
3491 38 : rw.parseBody(c);
3492 :
3493 : // Check if change refs point to this commit. Usually there are 0-1 change
3494 : // refs pointing to this commit.
3495 : for (PatchSet.Id psId :
3496 38 : receivePackRefCache.patchSetIdsFromObjectId(c.copy())) {
3497 10 : Optional<ChangeNotes> notes = getChangeNotes(psId.changeId());
3498 10 : if (notes.isPresent() && notes.get().getChange().getDest().equals(branch)) {
3499 10 : if (submissionId == null) {
3500 10 : submissionId = new SubmissionId(notes.get().getChange());
3501 : }
3502 10 : existingPatchSets++;
3503 10 : bu.addOp(
3504 10 : notes.get().getChangeId(), setPrivateOpFactory.create(false, null));
3505 10 : bu.addOp(
3506 10 : psId.changeId(),
3507 10 : mergedByPushOpFactory.create(
3508 : requestScopePropagator,
3509 : psId,
3510 : submissionId,
3511 : refName,
3512 10 : newTip.getId().getName()));
3513 10 : continue COMMIT;
3514 : }
3515 1 : }
3516 :
3517 : for (String changeId :
3518 38 : ChangeUtil.getChangeIdsFromFooter(c, urlFormatter.get())) {
3519 36 : if (changeDataByKey == null) {
3520 36 : changeDataByKey =
3521 : retryHelper
3522 36 : .changeIndexQuery(
3523 : "queryOpenChangesByKeyByBranch",
3524 36 : q -> openChangesByKeyByBranch(q, branch))
3525 36 : .call();
3526 : }
3527 :
3528 36 : ChangeData onto = changeDataByKey.get(Change.key(changeId.trim()));
3529 36 : if (onto != null) {
3530 6 : newPatchSets++;
3531 : // Hold onto this until we're done with the walk, as the call to
3532 : // req.validate below calls isMergedInto which resets the walk.
3533 6 : ChangeNotes ontoNotes = onto.notes();
3534 6 : ReplaceRequest req =
3535 6 : new ReplaceRequest(ontoNotes.getChangeId(), c, cmd, false);
3536 6 : req.notes = ontoNotes;
3537 6 : replaceAndClose.add(req);
3538 6 : continue COMMIT;
3539 : }
3540 38 : }
3541 : }
3542 :
3543 38 : for (ReplaceRequest req : replaceAndClose) {
3544 6 : Change.Id id = req.notes.getChangeId();
3545 6 : if (!req.validateNewPatchSetForAutoClose()) {
3546 0 : logger.atFine().log("Not closing %s because validation failed", id);
3547 0 : continue;
3548 : }
3549 6 : if (submissionId == null) {
3550 6 : submissionId = new SubmissionId(req.notes.getChange());
3551 : }
3552 6 : req.addOps(bu, null);
3553 6 : bu.addOp(id, setPrivateOpFactory.create(false, null));
3554 6 : bu.addOp(
3555 : id,
3556 : mergedByPushOpFactory
3557 6 : .create(
3558 : requestScopePropagator,
3559 : req.psId,
3560 : submissionId,
3561 : refName,
3562 6 : newTip.getId().getName())
3563 6 : .setPatchSetProvider(req.replaceOp::getPatchSet));
3564 6 : bu.addOp(id, new ChangeProgressOp(progress));
3565 6 : ids.add(id);
3566 6 : }
3567 :
3568 38 : logger.atFine().log(
3569 : "Auto-closing %d changes with existing patch sets and %d with new patch"
3570 : + " sets",
3571 : existingPatchSets, newPatchSets);
3572 38 : bu.execute();
3573 22 : } catch (IOException | StorageException | PermissionBackendException e) {
3574 0 : throw new StorageException("Failed to auto-close changes", e);
3575 38 : }
3576 :
3577 : // If we are here, we didn't throw UpdateException. Record the result.
3578 : // The ordering is indeterminate due to the HashSet; unfortunately, Change.Id
3579 : // doesn't
3580 : // fit into TreeSet.
3581 38 : ids.stream()
3582 38 : .forEach(
3583 6 : id -> result.addChange(ReceiveCommitsResult.ChangeStatus.AUTOCLOSED, id));
3584 :
3585 38 : return null;
3586 : })
3587 : // Use a multiple of the default timeout to account for inner retries that may otherwise
3588 : // eat up the whole timeout so that no time is left to retry this outer action.
3589 38 : .defaultTimeoutMultiplier(5)
3590 38 : .call();
3591 0 : } catch (RestApiException e) {
3592 0 : logger.atSevere().withCause(e).log("Can't insert patchset");
3593 0 : } catch (UpdateException e) {
3594 0 : logger.atSevere().withCause(e).log("Failed to auto-close changes");
3595 : } finally {
3596 38 : logger.atFine().log("Done auto-closing changes");
3597 : }
3598 : }
3599 38 : }
3600 :
3601 : private Optional<ChangeNotes> getChangeNotes(Change.Id changeId) {
3602 : try {
3603 11 : return Optional.of(notesFactory.createChecked(project.getNameKey(), changeId));
3604 0 : } catch (NoSuchChangeException e) {
3605 0 : return Optional.empty();
3606 : }
3607 : }
3608 :
3609 : private Map<Change.Key, ChangeData> openChangesByKeyByBranch(
3610 : InternalChangeQuery internalChangeQuery, BranchNameKey branch) {
3611 36 : try (TraceTimer traceTimer =
3612 36 : newTimer("openChangesByKeyByBranch", Metadata.builder().branchName(branch.branch()))) {
3613 36 : Map<Change.Key, ChangeData> r = new HashMap<>();
3614 36 : for (ChangeData cd : internalChangeQuery.byBranchOpen(branch)) {
3615 : try {
3616 : // ChangeData is not materialised into a ChangeNotes for avoiding
3617 : // to load a potentially large number of changes meta-data into memory
3618 : // which would cause unnecessary disk I/O, CPU and heap utilisation.
3619 12 : r.put(cd.change().getKey(), cd);
3620 0 : } catch (NoSuchChangeException e) {
3621 : // Ignore deleted change
3622 12 : }
3623 12 : }
3624 36 : return r;
3625 : }
3626 : }
3627 :
3628 : private TraceTimer newTimer(String name) {
3629 96 : return newTimer(getClass(), name);
3630 : }
3631 :
3632 : private TraceTimer newTimer(Class<?> clazz, String name) {
3633 96 : return newTimer(clazz, name, Metadata.builder());
3634 : }
3635 :
3636 : private TraceTimer newTimer(String name, Metadata.Builder metadataBuilder) {
3637 96 : return newTimer(getClass(), name, metadataBuilder);
3638 : }
3639 :
3640 : private TraceTimer newTimer(Class<?> clazz, String name, Metadata.Builder metadataBuilder) {
3641 96 : metadataBuilder.projectName(project.getName());
3642 96 : return TraceContext.newTimer(clazz.getSimpleName() + "#" + name, metadataBuilder.build());
3643 : }
3644 :
3645 : private static void reject(ReceiveCommand cmd, String why) {
3646 25 : logger.atFine().log("Rejecting command '%s': %s", cmd, why);
3647 25 : cmd.setResult(REJECTED_OTHER_REASON, why);
3648 25 : }
3649 :
3650 : private static void rejectRemaining(Collection<ReceiveCommand> commands, String why) {
3651 96 : rejectRemaining(commands.stream(), why);
3652 96 : }
3653 :
3654 : private static void rejectRemaining(Stream<ReceiveCommand> commands, String why) {
3655 96 : commands.filter(cmd -> cmd.getResult() == NOT_ATTEMPTED).forEach(cmd -> reject(cmd, why));
3656 96 : }
3657 :
3658 : private static boolean isHead(ReceiveCommand cmd) {
3659 49 : return cmd.getRefName().startsWith(Constants.R_HEADS);
3660 : }
3661 :
3662 : private static boolean isConfig(ReceiveCommand cmd) {
3663 47 : return cmd.getRefName().equals(RefNames.REFS_CONFIG);
3664 : }
3665 :
3666 : private static String commandToString(ReceiveCommand cmd) {
3667 5 : StringBuilder b = new StringBuilder();
3668 5 : b.append(cmd);
3669 5 : b.append(" (").append(cmd.getResult());
3670 5 : if (cmd.getMessage() != null) {
3671 0 : b.append(": ").append(cmd.getMessage());
3672 : }
3673 5 : b.append(")\n");
3674 5 : return b.toString();
3675 : }
3676 : }
|