Line data Source code
1 : // Copyright (C) 2015 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.change;
16 :
17 : import static com.google.common.base.Preconditions.checkState;
18 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
19 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
20 : import static java.util.Objects.requireNonNull;
21 :
22 : import com.google.common.collect.ImmutableList;
23 : import com.google.common.collect.ImmutableListMultimap;
24 : import com.google.common.collect.ImmutableSet;
25 : import com.google.gerrit.entities.PatchSet;
26 : import com.google.gerrit.extensions.restapi.BadRequestException;
27 : import com.google.gerrit.extensions.restapi.MergeConflictException;
28 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
29 : import com.google.gerrit.extensions.restapi.RestApiException;
30 : import com.google.gerrit.server.ChangeUtil;
31 : import com.google.gerrit.server.CurrentUser;
32 : import com.google.gerrit.server.IdentifiedUser;
33 : import com.google.gerrit.server.change.RebaseUtil.Base;
34 : import com.google.gerrit.server.git.CodeReviewCommit;
35 : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
36 : import com.google.gerrit.server.git.GroupCollector;
37 : import com.google.gerrit.server.git.MergeUtil;
38 : import com.google.gerrit.server.git.MergeUtilFactory;
39 : import com.google.gerrit.server.notedb.ChangeNotes;
40 : import com.google.gerrit.server.permissions.PermissionBackendException;
41 : import com.google.gerrit.server.project.InvalidChangeOperationException;
42 : import com.google.gerrit.server.project.NoSuchChangeException;
43 : import com.google.gerrit.server.project.ProjectCache;
44 : import com.google.gerrit.server.project.ProjectState;
45 : import com.google.gerrit.server.update.BatchUpdateOp;
46 : import com.google.gerrit.server.update.ChangeContext;
47 : import com.google.gerrit.server.update.PostUpdateContext;
48 : import com.google.gerrit.server.update.RepoContext;
49 : import com.google.inject.Inject;
50 : import com.google.inject.assistedinject.Assisted;
51 : import java.io.IOException;
52 : import java.util.List;
53 : import java.util.Map;
54 : import org.eclipse.jgit.diff.Sequence;
55 : import org.eclipse.jgit.dircache.DirCache;
56 : import org.eclipse.jgit.lib.CommitBuilder;
57 : import org.eclipse.jgit.lib.ObjectId;
58 : import org.eclipse.jgit.lib.PersonIdent;
59 : import org.eclipse.jgit.merge.MergeResult;
60 : import org.eclipse.jgit.merge.ResolveMerger;
61 : import org.eclipse.jgit.merge.ThreeWayMerger;
62 : import org.eclipse.jgit.revwalk.RevCommit;
63 : import org.eclipse.jgit.revwalk.RevWalk;
64 :
65 : /**
66 : * BatchUpdate operation that rebases a change.
67 : *
68 : * <p>Can only be executed in a {@link com.google.gerrit.server.update.BatchUpdate} set has a {@link
69 : * CodeReviewRevWalk} set as {@link RevWalk} (set via {@link
70 : * com.google.gerrit.server.update.BatchUpdate#setRepository(org.eclipse.jgit.lib.Repository,
71 : * RevWalk, org.eclipse.jgit.lib.ObjectInserter)}).
72 : */
73 : public class RebaseChangeOp implements BatchUpdateOp {
74 : public interface Factory {
75 : RebaseChangeOp create(ChangeNotes notes, PatchSet originalPatchSet, ObjectId baseCommitId);
76 : }
77 :
78 : private final PatchSetInserter.Factory patchSetInserterFactory;
79 : private final MergeUtilFactory mergeUtilFactory;
80 : private final RebaseUtil rebaseUtil;
81 : private final ChangeResource.Factory changeResourceFactory;
82 :
83 : private final ChangeNotes notes;
84 : private final PatchSet originalPatchSet;
85 : private final IdentifiedUser.GenericFactory identifiedUserFactory;
86 : private final ProjectCache projectCache;
87 :
88 : private ObjectId baseCommitId;
89 : private PersonIdent committerIdent;
90 13 : private boolean fireRevisionCreated = true;
91 13 : private boolean validate = true;
92 13 : private boolean checkAddPatchSetPermission = true;
93 : private boolean forceContentMerge;
94 : private boolean allowConflicts;
95 : private boolean detailedCommitMessage;
96 13 : private boolean postMessage = true;
97 13 : private boolean sendEmail = true;
98 13 : private boolean storeCopiedVotes = true;
99 13 : private boolean matchAuthorToCommitterDate = false;
100 13 : private ImmutableListMultimap<String, String> validationOptions = ImmutableListMultimap.of();
101 :
102 : private CodeReviewCommit rebasedCommit;
103 : private PatchSet.Id rebasedPatchSetId;
104 : private PatchSetInserter patchSetInserter;
105 : private PatchSet rebasedPatchSet;
106 :
107 : @Inject
108 : RebaseChangeOp(
109 : PatchSetInserter.Factory patchSetInserterFactory,
110 : MergeUtilFactory mergeUtilFactory,
111 : RebaseUtil rebaseUtil,
112 : ChangeResource.Factory changeResourceFactory,
113 : IdentifiedUser.GenericFactory identifiedUserFactory,
114 : ProjectCache projectCache,
115 : @Assisted ChangeNotes notes,
116 : @Assisted PatchSet originalPatchSet,
117 13 : @Assisted ObjectId baseCommitId) {
118 13 : this.patchSetInserterFactory = patchSetInserterFactory;
119 13 : this.mergeUtilFactory = mergeUtilFactory;
120 13 : this.rebaseUtil = rebaseUtil;
121 13 : this.changeResourceFactory = changeResourceFactory;
122 13 : this.identifiedUserFactory = identifiedUserFactory;
123 13 : this.projectCache = projectCache;
124 13 : this.notes = notes;
125 13 : this.originalPatchSet = originalPatchSet;
126 13 : this.baseCommitId = baseCommitId;
127 13 : }
128 :
129 : public RebaseChangeOp setCommitterIdent(PersonIdent committerIdent) {
130 0 : this.committerIdent = committerIdent;
131 0 : return this;
132 : }
133 :
134 : public RebaseChangeOp setValidate(boolean validate) {
135 4 : this.validate = validate;
136 4 : return this;
137 : }
138 :
139 : public RebaseChangeOp setCheckAddPatchSetPermission(boolean checkAddPatchSetPermission) {
140 4 : this.checkAddPatchSetPermission = checkAddPatchSetPermission;
141 4 : return this;
142 : }
143 :
144 : public RebaseChangeOp setFireRevisionCreated(boolean fireRevisionCreated) {
145 13 : this.fireRevisionCreated = fireRevisionCreated;
146 13 : return this;
147 : }
148 :
149 : public RebaseChangeOp setForceContentMerge(boolean forceContentMerge) {
150 11 : this.forceContentMerge = forceContentMerge;
151 11 : return this;
152 : }
153 :
154 : /**
155 : * Allows the rebase to succeed if there are conflicts.
156 : *
157 : * <p>This setting requires that {@link #forceContentMerge} is set {@code true}. If {@link
158 : * #forceContentMerge} is {@code false} this setting has no effect.
159 : *
160 : * @see #setForceContentMerge(boolean)
161 : */
162 : public RebaseChangeOp setAllowConflicts(boolean allowConflicts) {
163 11 : this.allowConflicts = allowConflicts;
164 11 : return this;
165 : }
166 :
167 : public RebaseChangeOp setDetailedCommitMessage(boolean detailedCommitMessage) {
168 4 : this.detailedCommitMessage = detailedCommitMessage;
169 4 : return this;
170 : }
171 :
172 : public RebaseChangeOp setPostMessage(boolean postMessage) {
173 4 : this.postMessage = postMessage;
174 4 : return this;
175 : }
176 :
177 : /**
178 : * We always want to store copied votes except when the change is getting submitted and a new
179 : * patch-set is created on submit (using submit strategies such as "REBASE_ALWAYS"). In such
180 : * cases, we already store the votes of the new patch-sets in SubmitStrategyOp#saveApprovals. We
181 : * should not also store the copied votes.
182 : */
183 : public RebaseChangeOp setStoreCopiedVotes(boolean storeCopiedVotes) {
184 4 : this.storeCopiedVotes = storeCopiedVotes;
185 4 : return this;
186 : }
187 :
188 : public RebaseChangeOp setSendEmail(boolean sendEmail) {
189 4 : this.sendEmail = sendEmail;
190 4 : return this;
191 : }
192 :
193 : public RebaseChangeOp setMatchAuthorToCommitterDate(boolean matchAuthorToCommitterDate) {
194 4 : this.matchAuthorToCommitterDate = matchAuthorToCommitterDate;
195 4 : return this;
196 : }
197 :
198 : public RebaseChangeOp setValidationOptions(
199 : ImmutableListMultimap<String, String> validationOptions) {
200 11 : requireNonNull(validationOptions, "validationOptions may not be null");
201 11 : this.validationOptions = validationOptions;
202 11 : return this;
203 : }
204 :
205 : @Override
206 : public void updateRepo(RepoContext ctx)
207 : throws MergeConflictException, InvalidChangeOperationException, RestApiException, IOException,
208 : NoSuchChangeException, PermissionBackendException {
209 : // Ok that originalPatchSet was not read in a transaction, since we just
210 : // need its revision.
211 13 : RevWalk rw = ctx.getRevWalk();
212 13 : RevCommit original = rw.parseCommit(originalPatchSet.commitId());
213 13 : rw.parseBody(original);
214 13 : RevCommit baseCommit = rw.parseCommit(baseCommitId);
215 13 : CurrentUser changeOwner = identifiedUserFactory.create(notes.getChange().getOwner());
216 :
217 : String newCommitMessage;
218 13 : if (detailedCommitMessage) {
219 3 : rw.parseBody(baseCommit);
220 3 : newCommitMessage =
221 3 : newMergeUtil()
222 3 : .createCommitMessageOnSubmit(original, baseCommit, notes, originalPatchSet.id());
223 : } else {
224 13 : newCommitMessage = original.getFullMessage();
225 : }
226 :
227 13 : rebasedCommit = rebaseCommit(ctx, original, baseCommit, newCommitMessage);
228 13 : Base base =
229 13 : rebaseUtil.parseBase(
230 : new RevisionResource(
231 13 : changeResourceFactory.create(notes, changeOwner), originalPatchSet),
232 13 : baseCommitId.name());
233 :
234 13 : rebasedPatchSetId =
235 13 : ChangeUtil.nextPatchSetIdFromChangeRefs(
236 13 : ctx.getRepoView().getRefs(originalPatchSet.id().changeId().toRefPrefix()).keySet(),
237 13 : notes.getChange().currentPatchSetId());
238 13 : patchSetInserter =
239 : patchSetInserterFactory
240 13 : .create(notes, rebasedPatchSetId, rebasedCommit)
241 13 : .setDescription("Rebase")
242 13 : .setFireRevisionCreated(fireRevisionCreated)
243 13 : .setCheckAddPatchSetPermission(checkAddPatchSetPermission)
244 13 : .setValidate(validate)
245 13 : .setSendEmail(sendEmail)
246 : // The votes are automatically copied and they don't count as copied votes. See
247 : // method's javadoc.
248 13 : .setStoreCopiedVotes(storeCopiedVotes);
249 :
250 13 : if (!rebasedCommit.getFilesWithGitConflicts().isEmpty()
251 1 : && !notes.getChange().isWorkInProgress()) {
252 1 : patchSetInserter.setWorkInProgress(true);
253 : }
254 :
255 13 : patchSetInserter.setValidationOptions(validationOptions);
256 :
257 13 : if (postMessage) {
258 11 : patchSetInserter.setMessage(
259 11 : messageForRebasedChange(rebasedPatchSetId, originalPatchSet.id(), rebasedCommit));
260 : }
261 :
262 13 : if (base != null && !base.notes().getChange().isMerged()) {
263 5 : if (!base.notes().getChange().isMerged()) {
264 : // Add to end of relation chain for open base change.
265 5 : patchSetInserter.setGroups(base.patchSet().groups());
266 : } else {
267 : // If the base is merged, start a new relation chain.
268 0 : patchSetInserter.setGroups(GroupCollector.getDefaultGroups(rebasedCommit));
269 : }
270 : }
271 :
272 13 : ctx.getRevWalk().getObjectReader().getCreatedFromInserter().flush();
273 13 : patchSetInserter.updateRepo(ctx);
274 13 : }
275 :
276 : private static String messageForRebasedChange(
277 : PatchSet.Id rebasePatchSetId, PatchSet.Id originalPatchSetId, CodeReviewCommit commit) {
278 11 : StringBuilder stringBuilder =
279 : new StringBuilder(
280 11 : String.format(
281 : "Patch Set %d: Patch Set %d was rebased",
282 11 : rebasePatchSetId.get(), originalPatchSetId.get()));
283 :
284 11 : if (!commit.getFilesWithGitConflicts().isEmpty()) {
285 1 : stringBuilder.append("\n\nThe following files contain Git conflicts:\n");
286 1 : commit.getFilesWithGitConflicts().stream()
287 1 : .sorted()
288 1 : .forEach(filePath -> stringBuilder.append("* ").append(filePath).append("\n"));
289 : }
290 :
291 11 : return stringBuilder.toString();
292 : }
293 :
294 : @Override
295 : public boolean updateChange(ChangeContext ctx)
296 : throws ResourceConflictException, IOException, BadRequestException {
297 13 : boolean ret = patchSetInserter.updateChange(ctx);
298 13 : rebasedPatchSet = patchSetInserter.getPatchSet();
299 13 : return ret;
300 : }
301 :
302 : @Override
303 : public void postUpdate(PostUpdateContext ctx) {
304 13 : patchSetInserter.postUpdate(ctx);
305 13 : }
306 :
307 : public CodeReviewCommit getRebasedCommit() {
308 13 : checkState(rebasedCommit != null, "getRebasedCommit() only valid after updateRepo");
309 13 : return rebasedCommit;
310 : }
311 :
312 : public PatchSet.Id getPatchSetId() {
313 4 : checkState(rebasedPatchSetId != null, "getPatchSetId() only valid after updateRepo");
314 4 : return rebasedPatchSetId;
315 : }
316 :
317 : public PatchSet getPatchSet() {
318 4 : checkState(rebasedPatchSet != null, "getPatchSet() only valid after executing update");
319 4 : return rebasedPatchSet;
320 : }
321 :
322 : private MergeUtil newMergeUtil() {
323 13 : ProjectState project =
324 13 : projectCache.get(notes.getProjectName()).orElseThrow(illegalState(notes.getProjectName()));
325 13 : return forceContentMerge
326 11 : ? mergeUtilFactory.create(project, true)
327 4 : : mergeUtilFactory.create(project);
328 : }
329 :
330 : /**
331 : * Rebase a commit.
332 : *
333 : * @param ctx repo context.
334 : * @param original the commit to rebase.
335 : * @param base base to rebase against.
336 : * @return the rebased commit.
337 : * @throws MergeConflictException the rebase failed due to a merge conflict.
338 : * @throws IOException the merge failed for another reason.
339 : */
340 : private CodeReviewCommit rebaseCommit(
341 : RepoContext ctx, RevCommit original, ObjectId base, String commitMessage)
342 : throws ResourceConflictException, IOException {
343 13 : RevCommit parentCommit = original.getParent(0);
344 :
345 13 : if (base.equals(parentCommit)) {
346 0 : throw new ResourceConflictException("Change is already up to date.");
347 : }
348 :
349 13 : ThreeWayMerger merger =
350 13 : newMergeUtil().newThreeWayMerger(ctx.getInserter(), ctx.getRepoView().getConfig());
351 13 : merger.setBase(parentCommit);
352 :
353 13 : DirCache dc = DirCache.newInCore();
354 13 : if (allowConflicts && merger instanceof ResolveMerger) {
355 : // The DirCache must be set on ResolveMerger before calling
356 : // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
357 1 : ((ResolveMerger) merger).setDirCache(dc);
358 : }
359 :
360 13 : boolean success = merger.merge(original, base);
361 :
362 : ObjectId tree;
363 : ImmutableSet<String> filesWithGitConflicts;
364 13 : if (success) {
365 13 : filesWithGitConflicts = null;
366 13 : tree = merger.getResultTreeId();
367 : } else {
368 3 : List<String> conflicts = ImmutableList.of();
369 3 : if (merger instanceof ResolveMerger) {
370 3 : conflicts = ((ResolveMerger) merger).getUnmergedPaths();
371 : }
372 :
373 3 : if (!allowConflicts || !(merger instanceof ResolveMerger)) {
374 3 : throw new MergeConflictException(
375 : "The change could not be rebased due to a conflict during merge.\n\n"
376 3 : + MergeUtil.createConflictMessage(conflicts));
377 : }
378 :
379 1 : Map<String, MergeResult<? extends Sequence>> mergeResults =
380 1 : ((ResolveMerger) merger).getMergeResults();
381 :
382 1 : filesWithGitConflicts =
383 1 : mergeResults.entrySet().stream()
384 1 : .filter(e -> e.getValue().containsConflicts())
385 1 : .map(Map.Entry::getKey)
386 1 : .collect(toImmutableSet());
387 :
388 1 : tree =
389 1 : MergeUtil.mergeWithConflicts(
390 1 : ctx.getRevWalk(),
391 1 : ctx.getInserter(),
392 : dc,
393 : "PATCH SET",
394 : original,
395 : "BASE",
396 1 : ctx.getRevWalk().parseCommit(base),
397 : mergeResults);
398 : }
399 :
400 13 : CommitBuilder cb = new CommitBuilder();
401 13 : cb.setTreeId(tree);
402 13 : cb.setParentId(base);
403 13 : cb.setAuthor(original.getAuthorIdent());
404 13 : cb.setMessage(commitMessage);
405 13 : if (committerIdent != null) {
406 0 : cb.setCommitter(committerIdent);
407 : } else {
408 13 : cb.setCommitter(ctx.newCommitterIdent());
409 : }
410 13 : if (matchAuthorToCommitterDate) {
411 1 : cb.setAuthor(
412 : new PersonIdent(
413 1 : cb.getAuthor(), cb.getCommitter().getWhen(), cb.getCommitter().getTimeZone()));
414 : }
415 13 : ObjectId objectId = ctx.getInserter().insert(cb);
416 13 : ctx.getInserter().flush();
417 13 : CodeReviewCommit commit = ((CodeReviewRevWalk) ctx.getRevWalk()).parseCommit(objectId);
418 13 : commit.setFilesWithGitConflicts(filesWithGitConflicts);
419 13 : return commit;
420 : }
421 : }
|