Line data Source code
1 : // Copyright (C) 2012 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.restapi.change;
16 :
17 : import static com.google.common.base.MoreObjects.firstNonNull;
18 : import static com.google.gerrit.server.project.ProjectCache.noSuchProject;
19 :
20 : import com.google.auto.value.AutoValue;
21 : import com.google.common.base.Strings;
22 : import com.google.common.collect.ImmutableListMultimap;
23 : import com.google.common.collect.ImmutableSet;
24 : import com.google.gerrit.common.Nullable;
25 : import com.google.gerrit.entities.Account;
26 : import com.google.gerrit.entities.BranchNameKey;
27 : import com.google.gerrit.entities.Change;
28 : import com.google.gerrit.entities.PatchSet;
29 : import com.google.gerrit.entities.Project;
30 : import com.google.gerrit.extensions.api.changes.CherryPickInput;
31 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
32 : import com.google.gerrit.extensions.restapi.BadRequestException;
33 : import com.google.gerrit.extensions.restapi.MergeConflictException;
34 : import com.google.gerrit.extensions.restapi.RestApiException;
35 : import com.google.gerrit.server.ChangeUtil;
36 : import com.google.gerrit.server.GerritPersonIdent;
37 : import com.google.gerrit.server.IdentifiedUser;
38 : import com.google.gerrit.server.ReviewerSet;
39 : import com.google.gerrit.server.approval.ApprovalsUtil;
40 : import com.google.gerrit.server.change.ChangeInserter;
41 : import com.google.gerrit.server.change.NotifyResolver;
42 : import com.google.gerrit.server.change.PatchSetInserter;
43 : import com.google.gerrit.server.change.ResetCherryPickOp;
44 : import com.google.gerrit.server.change.SetCherryPickOp;
45 : import com.google.gerrit.server.git.CodeReviewCommit;
46 : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
47 : import com.google.gerrit.server.git.CommitUtil;
48 : import com.google.gerrit.server.git.GitRepositoryManager;
49 : import com.google.gerrit.server.git.GroupCollector;
50 : import com.google.gerrit.server.git.MergeUtil;
51 : import com.google.gerrit.server.git.MergeUtilFactory;
52 : import com.google.gerrit.server.notedb.ChangeNotes;
53 : import com.google.gerrit.server.notedb.ReviewerStateInternal;
54 : import com.google.gerrit.server.notedb.Sequences;
55 : import com.google.gerrit.server.project.InvalidChangeOperationException;
56 : import com.google.gerrit.server.project.NoSuchProjectException;
57 : import com.google.gerrit.server.project.ProjectCache;
58 : import com.google.gerrit.server.project.ProjectState;
59 : import com.google.gerrit.server.query.change.ChangeData;
60 : import com.google.gerrit.server.query.change.InternalChangeQuery;
61 : import com.google.gerrit.server.submit.IntegrationConflictException;
62 : import com.google.gerrit.server.submit.MergeIdenticalTreeException;
63 : import com.google.gerrit.server.update.BatchUpdate;
64 : import com.google.gerrit.server.update.UpdateException;
65 : import com.google.gerrit.server.util.CommitMessageUtil;
66 : import com.google.gerrit.server.util.time.TimeUtil;
67 : import com.google.inject.Inject;
68 : import com.google.inject.Provider;
69 : import com.google.inject.Singleton;
70 : import java.io.IOException;
71 : import java.time.Instant;
72 : import java.time.ZoneId;
73 : import java.util.HashSet;
74 : import java.util.List;
75 : import java.util.Map;
76 : import java.util.Set;
77 : import org.eclipse.jgit.errors.ConfigInvalidException;
78 : import org.eclipse.jgit.lib.ObjectId;
79 : import org.eclipse.jgit.lib.ObjectInserter;
80 : import org.eclipse.jgit.lib.ObjectReader;
81 : import org.eclipse.jgit.lib.PersonIdent;
82 : import org.eclipse.jgit.lib.Ref;
83 : import org.eclipse.jgit.lib.Repository;
84 : import org.eclipse.jgit.revwalk.RevCommit;
85 : import org.eclipse.jgit.util.ChangeIdUtil;
86 :
87 : @Singleton
88 : public class CherryPickChange {
89 : @AutoValue
90 8 : abstract static class Result {
91 : static Result create(Change.Id changeId, ImmutableSet<String> filesWithGitConflicts) {
92 8 : return new AutoValue_CherryPickChange_Result(changeId, filesWithGitConflicts);
93 : }
94 :
95 : abstract Change.Id changeId();
96 :
97 : abstract ImmutableSet<String> filesWithGitConflicts();
98 : }
99 :
100 : private final Sequences seq;
101 : private final Provider<InternalChangeQuery> queryProvider;
102 : private final GitRepositoryManager gitManager;
103 : private final ZoneId serverZoneId;
104 : private final Provider<IdentifiedUser> user;
105 : private final ChangeInserter.Factory changeInserterFactory;
106 : private final PatchSetInserter.Factory patchSetInserterFactory;
107 : private final SetCherryPickOp.Factory setCherryPickOfFactory;
108 : private final MergeUtilFactory mergeUtilFactory;
109 : private final ChangeNotes.Factory changeNotesFactory;
110 : private final ProjectCache projectCache;
111 : private final ApprovalsUtil approvalsUtil;
112 : private final NotifyResolver notifyResolver;
113 : private final BatchUpdate.Factory batchUpdateFactory;
114 :
115 : @Inject
116 : CherryPickChange(
117 : Sequences seq,
118 : Provider<InternalChangeQuery> queryProvider,
119 : @GerritPersonIdent PersonIdent myIdent,
120 : GitRepositoryManager gitManager,
121 : Provider<IdentifiedUser> user,
122 : ChangeInserter.Factory changeInserterFactory,
123 : PatchSetInserter.Factory patchSetInserterFactory,
124 : SetCherryPickOp.Factory setCherryPickOfFactory,
125 : MergeUtilFactory mergeUtilFactory,
126 : ChangeNotes.Factory changeNotesFactory,
127 : ProjectCache projectCache,
128 : ApprovalsUtil approvalsUtil,
129 : NotifyResolver notifyResolver,
130 145 : BatchUpdate.Factory batchUpdateFactory) {
131 145 : this.seq = seq;
132 145 : this.queryProvider = queryProvider;
133 145 : this.gitManager = gitManager;
134 145 : this.serverZoneId = myIdent.getZoneId();
135 145 : this.user = user;
136 145 : this.changeInserterFactory = changeInserterFactory;
137 145 : this.patchSetInserterFactory = patchSetInserterFactory;
138 145 : this.setCherryPickOfFactory = setCherryPickOfFactory;
139 145 : this.mergeUtilFactory = mergeUtilFactory;
140 145 : this.changeNotesFactory = changeNotesFactory;
141 145 : this.projectCache = projectCache;
142 145 : this.approvalsUtil = approvalsUtil;
143 145 : this.notifyResolver = notifyResolver;
144 145 : this.batchUpdateFactory = batchUpdateFactory;
145 145 : }
146 :
147 : /**
148 : * This function is used for cherry picking a change.
149 : *
150 : * @param change Change to cherry pick.
151 : * @param patch The patch of that change.
152 : * @param input Input object for different configurations of cherry pick.
153 : * @param dest Destination branch for the cherry pick.
154 : * @return Result object that describes the cherry pick.
155 : * @throws IOException Unable to open repository or read from the database.
156 : * @throws InvalidChangeOperationException Parent or branch don't exist, or two changes with same
157 : * key exist in the branch.
158 : * @throws UpdateException Problem updating the database using batchUpdateFactory.
159 : * @throws RestApiException Error such as invalid SHA1
160 : * @throws ConfigInvalidException Can't find account to notify.
161 : * @throws NoSuchProjectException Can't find project state.
162 : */
163 : public Result cherryPick(Change change, PatchSet patch, CherryPickInput input, BranchNameKey dest)
164 : throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
165 : ConfigInvalidException, NoSuchProjectException {
166 6 : return cherryPick(
167 : change,
168 6 : change.getProject(),
169 6 : patch.commitId(),
170 : input,
171 : dest,
172 6 : TimeUtil.now(),
173 : null,
174 : null,
175 : null,
176 : null);
177 : }
178 :
179 : /**
180 : * This function is called directly to cherry pick a commit. Also, it is used to cherry pick a
181 : * change as well as long as sourceChange is not null.
182 : *
183 : * @param sourceChange Change to cherry pick. Can be null, and then the function will only cherry
184 : * pick a commit.
185 : * @param project Project name
186 : * @param sourceCommit Id of the commit to be cherry picked.
187 : * @param input Input object for different configurations of cherry pick.
188 : * @param dest Destination branch for the cherry pick.
189 : * @return Result object that describes the cherry pick.
190 : * @throws IOException Unable to open repository or read from the database.
191 : * @throws InvalidChangeOperationException Parent or branch don't exist, or two changes with same
192 : * key exist in the branch.
193 : * @throws UpdateException Problem updating the database using batchUpdateFactory.
194 : * @throws RestApiException Error such as invalid SHA1
195 : * @throws ConfigInvalidException Can't find account to notify.
196 : * @throws NoSuchProjectException Can't find project state.
197 : */
198 : public Result cherryPick(
199 : @Nullable Change sourceChange,
200 : Project.NameKey project,
201 : ObjectId sourceCommit,
202 : CherryPickInput input,
203 : BranchNameKey dest)
204 : throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
205 : ConfigInvalidException, NoSuchProjectException {
206 2 : return cherryPick(
207 2 : sourceChange, project, sourceCommit, input, dest, TimeUtil.now(), null, null, null, null);
208 : }
209 :
210 : /**
211 : * This function can be called directly to cherry-pick a change (or commit if sourceChange is
212 : * null) with a few other parameters that are especially useful for cherry-picking a commit that
213 : * is the revert-of another change.
214 : *
215 : * @param sourceChange Change to cherry pick. Can be null, and then the function will only cherry
216 : * pick a commit.
217 : * @param project Project name
218 : * @param sourceCommit Id of the commit to be cherry picked.
219 : * @param input Input object for different configurations of cherry pick.
220 : * @param dest Destination branch for the cherry pick.
221 : * @param timestamp the current timestamp.
222 : * @param revertedChange The id of the change that is reverted. This is used for the "revertOf"
223 : * field to mark the created cherry pick change as "revertOf" the original change that was
224 : * reverted.
225 : * @param changeIdForNewChange The Change-Id that the new change of the cherry pick will have.
226 : * @param idForNewChange The ID that the new change of the cherry pick will have. If provided and
227 : * the cherry-pick doesn't result in creating a new change, then
228 : * InvalidChangeOperationException is thrown.
229 : * @return Result object that describes the cherry pick.
230 : * @throws IOException Unable to open repository or read from the database.
231 : * @throws InvalidChangeOperationException Parent or branch don't exist, or two changes with same
232 : * key exist in the branch. Also thrown when idForNewChange is not null but cherry-pick only
233 : * creates a new patchset rather than a new change.
234 : * @throws UpdateException Problem updating the database using batchUpdateFactory.
235 : * @throws RestApiException Error such as invalid SHA1
236 : * @throws ConfigInvalidException Can't find account to notify.
237 : * @throws NoSuchProjectException Can't find project state.
238 : */
239 : public Result cherryPick(
240 : @Nullable Change sourceChange,
241 : Project.NameKey project,
242 : ObjectId sourceCommit,
243 : CherryPickInput input,
244 : BranchNameKey dest,
245 : Instant timestamp,
246 : @Nullable Change.Id revertedChange,
247 : @Nullable ObjectId changeIdForNewChange,
248 : @Nullable Change.Id idForNewChange,
249 : @Nullable Boolean workInProgress)
250 : throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
251 : ConfigInvalidException, NoSuchProjectException {
252 8 : IdentifiedUser identifiedUser = user.get();
253 8 : try (Repository git = gitManager.openRepository(project);
254 : // This inserter and revwalk *must* be passed to any BatchUpdates
255 : // created later on, to ensure the cherry-picked commit is flushed
256 : // before patch sets are updated.
257 8 : ObjectInserter oi = git.newObjectInserter();
258 8 : ObjectReader reader = oi.newReader();
259 8 : CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) {
260 8 : Ref destRef = git.getRefDatabase().exactRef(dest.branch());
261 8 : if (destRef == null) {
262 1 : throw new InvalidChangeOperationException(
263 1 : String.format("Branch %s does not exist.", dest.branch()));
264 : }
265 :
266 8 : RevCommit baseCommit =
267 8 : CommitUtil.getBaseCommit(
268 8 : project.get(), queryProvider.get(), revWalk, destRef, input.base);
269 :
270 8 : CodeReviewCommit commitToCherryPick = revWalk.parseCommit(sourceCommit);
271 :
272 8 : if (input.parent <= 0 || input.parent > commitToCherryPick.getParentCount()) {
273 1 : throw new InvalidChangeOperationException(
274 1 : String.format(
275 : "Cherry Pick: Parent %s does not exist. Please specify a parent in"
276 : + " range [1, %s].",
277 1 : input.parent, commitToCherryPick.getParentCount()));
278 : }
279 :
280 : // If the commit message is not set, the commit message of the source commit will be used.
281 8 : String commitMessage = Strings.nullToEmpty(input.message);
282 8 : commitMessage = commitMessage.isEmpty() ? commitToCherryPick.getFullMessage() : commitMessage;
283 :
284 8 : String destChangeId = getDestinationChangeId(commitMessage, changeIdForNewChange);
285 :
286 8 : ChangeData destChange = null;
287 8 : if (destChangeId != null) {
288 : // If "idForNewChange" is not null we must fail, since we are not expecting an already
289 : // existing change.
290 5 : destChange = getDestChangeWithVerification(destChangeId, dest, idForNewChange != null);
291 : }
292 :
293 8 : if (changeIdForNewChange != null) {
294 : // If Change-Id was explicitly provided for the new change, override the value in commit
295 : // message.
296 1 : commitMessage = ChangeIdUtil.insertId(commitMessage, changeIdForNewChange, true);
297 7 : } else if (destChangeId == null) {
298 : // If commit message did not specify Change-Id, generate a new one and insert to the
299 : // message.
300 6 : commitMessage =
301 6 : ChangeIdUtil.insertId(commitMessage, CommitMessageUtil.generateChangeId(), true);
302 : }
303 8 : commitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(commitMessage);
304 :
305 : CodeReviewCommit cherryPickCommit;
306 8 : ProjectState projectState =
307 8 : projectCache.get(dest.project()).orElseThrow(noSuchProject(dest.project()));
308 8 : PersonIdent committerIdent = identifiedUser.newCommitterIdent(timestamp, serverZoneId);
309 :
310 : try {
311 : MergeUtil mergeUtil;
312 8 : if (input.allowConflicts) {
313 : // allowConflicts requires to use content merge
314 3 : mergeUtil = mergeUtilFactory.create(projectState, true);
315 : } else {
316 : // use content merge only if it's configured on the project
317 7 : mergeUtil = mergeUtilFactory.create(projectState);
318 : }
319 8 : cherryPickCommit =
320 8 : mergeUtil.createCherryPickFromCommit(
321 : oi,
322 8 : git.getConfig(),
323 : baseCommit,
324 : commitToCherryPick,
325 : committerIdent,
326 : commitMessage,
327 : revWalk,
328 8 : input.parent - 1,
329 : input.allowEmpty,
330 : input.allowConflicts);
331 8 : oi.flush();
332 1 : } catch (MergeIdenticalTreeException | MergeConflictException e) {
333 1 : throw new IntegrationConflictException("Cherry pick failed: " + e.getMessage(), e);
334 8 : }
335 :
336 8 : try (BatchUpdate bu = batchUpdateFactory.create(project, identifiedUser, timestamp)) {
337 8 : bu.setRepository(git, revWalk, oi);
338 8 : bu.setNotify(resolveNotify(input));
339 : Change.Id changeId;
340 8 : String newTopic = null;
341 8 : if (input.topic != null) {
342 3 : newTopic = Strings.emptyToNull(input.topic.trim());
343 : }
344 8 : if (newTopic == null
345 : && sourceChange != null
346 6 : && !Strings.isNullOrEmpty(sourceChange.getTopic())) {
347 1 : newTopic = sourceChange.getTopic() + "-" + dest.shortName();
348 : }
349 8 : if (destChange != null) {
350 : // The change key exists on the destination branch. The cherry pick
351 : // will be added as a new patch set.
352 3 : changeId =
353 3 : insertPatchSet(
354 : bu,
355 : git,
356 3 : destChange.notes(),
357 : cherryPickCommit,
358 : sourceChange,
359 : newTopic,
360 : input,
361 : workInProgress);
362 : } else {
363 : // Change key not found on destination branch. We can create a new
364 : // change.
365 7 : changeId =
366 7 : createNewChange(
367 : bu,
368 : cherryPickCommit,
369 7 : dest.branch(),
370 : newTopic,
371 : project,
372 : sourceChange,
373 : sourceCommit,
374 : input,
375 : revertedChange,
376 : idForNewChange,
377 : workInProgress);
378 : }
379 8 : bu.execute();
380 8 : return Result.create(changeId, cherryPickCommit.getFilesWithGitConflicts());
381 : }
382 : }
383 : }
384 :
385 : private Change.Id insertPatchSet(
386 : BatchUpdate bu,
387 : Repository git,
388 : ChangeNotes destNotes,
389 : CodeReviewCommit cherryPickCommit,
390 : @Nullable Change sourceChange,
391 : String topic,
392 : CherryPickInput input,
393 : @Nullable Boolean workInProgress)
394 : throws IOException {
395 3 : Change destChange = destNotes.getChange();
396 3 : PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
397 3 : PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, cherryPickCommit);
398 3 : inserter.setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".");
399 3 : inserter.setTopic(topic);
400 3 : if (workInProgress != null) {
401 0 : inserter.setWorkInProgress(workInProgress);
402 : }
403 3 : if (shouldSetToReady(cherryPickCommit, destNotes, workInProgress)) {
404 1 : inserter.setWorkInProgress(false);
405 : }
406 3 : inserter.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
407 3 : bu.addOp(destChange.getId(), inserter);
408 3 : PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId();
409 : // If sourceChange is not provided, reset cherryPickOf to avoid stale value.
410 3 : if (sourcePatchSetId == null) {
411 1 : bu.addOp(destChange.getId(), new ResetCherryPickOp());
412 3 : } else if (destChange.getCherryPickOf() == null
413 1 : || !destChange.getCherryPickOf().equals(sourcePatchSetId)) {
414 3 : SetCherryPickOp cherryPickOfUpdater = setCherryPickOfFactory.create(sourcePatchSetId);
415 3 : bu.addOp(destChange.getId(), cherryPickOfUpdater);
416 : }
417 3 : return destChange.getId();
418 : }
419 :
420 : /**
421 : * We should set the change to be "ready for review" if: 1. workInProgress is not already set on
422 : * this request. 2. The patch-set doesn't have any git conflict markers. 3. The change used to be
423 : * work in progress (because of a previous patch-set).
424 : */
425 : private boolean shouldSetToReady(
426 : CodeReviewCommit cherryPickCommit,
427 : ChangeNotes destChangeNotes,
428 : @Nullable Boolean workInProgress) {
429 3 : return workInProgress == null
430 3 : && cherryPickCommit.getFilesWithGitConflicts().isEmpty()
431 3 : && destChangeNotes.getChange().isWorkInProgress();
432 : }
433 :
434 : private Change.Id createNewChange(
435 : BatchUpdate bu,
436 : CodeReviewCommit cherryPickCommit,
437 : String refName,
438 : String topic,
439 : Project.NameKey project,
440 : @Nullable Change sourceChange,
441 : @Nullable ObjectId sourceCommit,
442 : CherryPickInput input,
443 : @Nullable Change.Id revertOf,
444 : @Nullable Change.Id idForNewChange,
445 : @Nullable Boolean workInProgress)
446 : throws IOException, InvalidChangeOperationException {
447 7 : Change.Id changeId = idForNewChange != null ? idForNewChange : Change.id(seq.nextChangeId());
448 7 : ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
449 7 : ins.setRevertOf(revertOf);
450 7 : if (workInProgress != null) {
451 1 : ins.setWorkInProgress(workInProgress);
452 : } else {
453 6 : ins.setWorkInProgress(
454 6 : (sourceChange != null && sourceChange.isWorkInProgress())
455 6 : || !cherryPickCommit.getFilesWithGitConflicts().isEmpty());
456 : }
457 7 : ins.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
458 7 : BranchNameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
459 7 : PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId();
460 7 : ins.setMessage(
461 7 : revertOf == null
462 6 : ? messageForDestinationChange(
463 6 : ins.getPatchSetId(), sourceBranch, sourceCommit, cherryPickCommit)
464 1 : : "Uploaded patch set 1.") // For revert commits, the message should not include
465 : // cherry-pick information.
466 7 : .setTopic(topic);
467 7 : if (revertOf == null) {
468 6 : ins.setCherryPickOf(sourcePatchSetId);
469 : }
470 7 : if (input.keepReviewers && sourceChange != null) {
471 2 : ReviewerSet reviewerSet =
472 2 : approvalsUtil.getReviewers(changeNotesFactory.createChecked(sourceChange));
473 2 : Set<Account.Id> reviewers =
474 2 : new HashSet<>(reviewerSet.byState(ReviewerStateInternal.REVIEWER));
475 2 : reviewers.add(sourceChange.getOwner());
476 2 : reviewers.remove(user.get().getAccountId());
477 2 : Set<Account.Id> ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC));
478 2 : ccs.remove(user.get().getAccountId());
479 2 : ins.setReviewersAndCcsIgnoreVisibility(reviewers, ccs);
480 : }
481 : // If there is a base, and the base is not merged, the groups will be overridden by the base's
482 : // groups.
483 7 : ins.setGroups(GroupCollector.getDefaultGroups(cherryPickCommit.getId()));
484 7 : if (input.base != null) {
485 3 : List<ChangeData> changes =
486 3 : queryProvider.get().setLimit(2).byBranchCommitOpen(project.get(), refName, input.base);
487 3 : if (changes.size() > 1) {
488 0 : throw new InvalidChangeOperationException(
489 : "Several changes with key "
490 : + input.base
491 : + " reside on the same branch. "
492 : + "Cannot cherry-pick on target branch.");
493 : }
494 3 : if (changes.size() == 1) {
495 3 : Change change = changes.get(0).change();
496 3 : ins.setGroups(changeNotesFactory.createChecked(change).getCurrentPatchSet().groups());
497 : }
498 : }
499 7 : bu.insertChange(ins);
500 7 : return changeId;
501 : }
502 :
503 : private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
504 : @Nullable Map<String, String> validationOptions) {
505 8 : if (validationOptions == null) {
506 8 : return ImmutableListMultimap.of();
507 : }
508 :
509 : ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
510 1 : ImmutableListMultimap.builder();
511 1 : validationOptions
512 1 : .entrySet()
513 1 : .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
514 1 : return validationOptionsBuilder.build();
515 : }
516 :
517 : private NotifyResolver.Result resolveNotify(CherryPickInput input)
518 : throws BadRequestException, ConfigInvalidException, IOException {
519 8 : return notifyResolver.resolve(
520 8 : firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
521 : }
522 :
523 : private String messageForDestinationChange(
524 : PatchSet.Id patchSetId,
525 : BranchNameKey sourceBranch,
526 : ObjectId sourceCommit,
527 : CodeReviewCommit cherryPickCommit) {
528 6 : StringBuilder stringBuilder = new StringBuilder("Patch Set ").append(patchSetId.get());
529 6 : if (sourceBranch != null) {
530 4 : stringBuilder.append(": Cherry Picked from branch ").append(sourceBranch.shortName());
531 : } else {
532 2 : stringBuilder.append(": Cherry Picked from commit ").append(sourceCommit.getName());
533 : }
534 6 : stringBuilder.append(".");
535 :
536 6 : if (!cherryPickCommit.getFilesWithGitConflicts().isEmpty()) {
537 2 : stringBuilder.append("\n\nThe following files contain Git conflicts:");
538 2 : cherryPickCommit.getFilesWithGitConflicts().stream()
539 2 : .sorted()
540 2 : .forEach(filePath -> stringBuilder.append("\n* ").append(filePath));
541 : }
542 :
543 6 : return stringBuilder.toString();
544 : }
545 :
546 : /**
547 : * Returns the Change-Id of destination change (as intended by the caller of cherry-pick
548 : * operation).
549 : *
550 : * <p>The Change-Id can be provided in one of the following ways:
551 : *
552 : * <ul>
553 : * <li>Explicitly provided for the new change.
554 : * <li>Provided in the input commit message.
555 : * <li>Taken from the source commit if commit message was not set.
556 : * </ul>
557 : *
558 : * Otherwise should be generated.
559 : *
560 : * @param commitMessage the commit message, as intended by the caller of cherry-pick operation.
561 : * @param changeIdForNewChange the explicitly provided Change-Id for the new change.
562 : * @return The Change-Id of destination change, {@code null} if Change-Id was not provided by the
563 : * caller of cherry-pick operation and should be generated.
564 : */
565 : @Nullable
566 : private String getDestinationChangeId(
567 : String commitMessage, @Nullable ObjectId changeIdForNewChange) {
568 8 : if (changeIdForNewChange != null) {
569 1 : return CommitMessageUtil.getChangeIdFromObjectId(changeIdForNewChange);
570 : }
571 7 : return CommitMessageUtil.getChangeIdFromCommitMessageFooter(commitMessage).orElse(null);
572 : }
573 :
574 : /**
575 : * Returns the change from the destination branch, if it exists and is valid for the cherry-pick.
576 : *
577 : * @param destChangeId the Change-ID of the change in the destination branch.
578 : * @param destBranch the branch to search by the Change-ID.
579 : * @param verifyIsMissing if {@code true}, verifies that the change should be missing in the
580 : * destination branch.
581 : * @return the verified change or {@code null} if the change was not found.
582 : * @throws InvalidChangeOperationException if the change was found but failed validation
583 : */
584 : @Nullable
585 : private ChangeData getDestChangeWithVerification(
586 : String destChangeId, BranchNameKey destBranch, boolean verifyIsMissing)
587 : throws InvalidChangeOperationException {
588 5 : List<ChangeData> destChanges =
589 5 : queryProvider.get().setLimit(2).byBranchKey(destBranch, Change.key(destChangeId));
590 5 : if (destChanges.size() > 1) {
591 0 : throw new InvalidChangeOperationException(
592 : "Several changes with key "
593 : + destChangeId
594 : + " reside on the same branch. "
595 : + "Cannot create a new patch set.");
596 : }
597 5 : if (destChanges.size() == 1 && verifyIsMissing) {
598 0 : throw new InvalidChangeOperationException(
599 0 : String.format(
600 : "Expected that cherry-pick with Change-Id %s to branch %s "
601 : + "in project %s creates a new change, but found existing change %d",
602 : destChangeId,
603 0 : destBranch.branch(),
604 0 : destBranch.project().get(),
605 0 : destChanges.get(0).getId().get()));
606 : }
607 5 : ChangeData destChange = destChanges.size() == 1 ? destChanges.get(0) : null;
608 :
609 5 : if (destChange != null && destChange.change().isClosed()) {
610 2 : throw new InvalidChangeOperationException(
611 2 : String.format(
612 : "Cherry-pick with Change-Id %s could not update the existing change %d "
613 : + "in destination branch %s of project %s, because the change was closed (%s)",
614 : destChangeId,
615 2 : destChange.getId().get(),
616 2 : destBranch.branch(),
617 2 : destBranch.project(),
618 2 : destChange.change().getStatus().name()));
619 : }
620 5 : return destChange;
621 : }
622 : }
|