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.git;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 : import static com.google.common.base.Preconditions.checkState;
19 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
20 : import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
21 : import static com.google.gerrit.git.ObjectIds.abbreviateName;
22 : import static java.nio.charset.StandardCharsets.UTF_8;
23 : import static java.util.Comparator.naturalOrder;
24 : import static java.util.stream.Collectors.joining;
25 :
26 : import com.google.auto.factory.AutoFactory;
27 : import com.google.auto.factory.Provided;
28 : import com.google.common.base.Strings;
29 : import com.google.common.collect.ImmutableList;
30 : import com.google.common.collect.ImmutableSet;
31 : import com.google.common.collect.ImmutableSortedSet;
32 : import com.google.common.collect.Iterables;
33 : import com.google.common.collect.Sets;
34 : import com.google.common.flogger.FluentLogger;
35 : import com.google.gerrit.common.FooterConstants;
36 : import com.google.gerrit.common.Nullable;
37 : import com.google.gerrit.entities.Account;
38 : import com.google.gerrit.entities.BooleanProjectConfig;
39 : import com.google.gerrit.entities.BranchNameKey;
40 : import com.google.gerrit.entities.Change;
41 : import com.google.gerrit.entities.LabelId;
42 : import com.google.gerrit.entities.LabelType;
43 : import com.google.gerrit.entities.PatchSet;
44 : import com.google.gerrit.entities.PatchSetApproval;
45 : import com.google.gerrit.exceptions.InvalidMergeStrategyException;
46 : import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
47 : import com.google.gerrit.exceptions.StorageException;
48 : import com.google.gerrit.extensions.registration.DynamicItem;
49 : import com.google.gerrit.extensions.restapi.BadRequestException;
50 : import com.google.gerrit.extensions.restapi.MergeConflictException;
51 : import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
52 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
53 : import com.google.gerrit.server.ChangeUtil;
54 : import com.google.gerrit.server.IdentifiedUser;
55 : import com.google.gerrit.server.approval.ApprovalsUtil;
56 : import com.google.gerrit.server.config.GerritServerConfig;
57 : import com.google.gerrit.server.config.UrlFormatter;
58 : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
59 : import com.google.gerrit.server.notedb.ChangeNotes;
60 : import com.google.gerrit.server.project.ProjectState;
61 : import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
62 : import com.google.gerrit.server.submit.CommitMergeStatus;
63 : import com.google.gerrit.server.submit.MergeIdenticalTreeException;
64 : import com.google.gerrit.server.submit.MergeSorter;
65 : import java.io.IOException;
66 : import java.io.InputStream;
67 : import java.util.ArrayList;
68 : import java.util.Collection;
69 : import java.util.Collections;
70 : import java.util.HashMap;
71 : import java.util.Iterator;
72 : import java.util.List;
73 : import java.util.Map;
74 : import java.util.Objects;
75 : import java.util.Optional;
76 : import java.util.Set;
77 : import org.eclipse.jgit.diff.Sequence;
78 : import org.eclipse.jgit.dircache.DirCache;
79 : import org.eclipse.jgit.dircache.DirCacheBuilder;
80 : import org.eclipse.jgit.dircache.DirCacheEntry;
81 : import org.eclipse.jgit.errors.AmbiguousObjectException;
82 : import org.eclipse.jgit.errors.IncorrectObjectTypeException;
83 : import org.eclipse.jgit.errors.LargeObjectException;
84 : import org.eclipse.jgit.errors.MissingObjectException;
85 : import org.eclipse.jgit.errors.NoMergeBaseException;
86 : import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
87 : import org.eclipse.jgit.errors.RevisionSyntaxException;
88 : import org.eclipse.jgit.lib.CommitBuilder;
89 : import org.eclipse.jgit.lib.Config;
90 : import org.eclipse.jgit.lib.Constants;
91 : import org.eclipse.jgit.lib.ObjectId;
92 : import org.eclipse.jgit.lib.ObjectInserter;
93 : import org.eclipse.jgit.lib.PersonIdent;
94 : import org.eclipse.jgit.lib.Repository;
95 : import org.eclipse.jgit.merge.MergeFormatter;
96 : import org.eclipse.jgit.merge.MergeResult;
97 : import org.eclipse.jgit.merge.MergeStrategy;
98 : import org.eclipse.jgit.merge.Merger;
99 : import org.eclipse.jgit.merge.ResolveMerger;
100 : import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
101 : import org.eclipse.jgit.merge.ThreeWayMerger;
102 : import org.eclipse.jgit.revwalk.FooterKey;
103 : import org.eclipse.jgit.revwalk.FooterLine;
104 : import org.eclipse.jgit.revwalk.RevCommit;
105 : import org.eclipse.jgit.revwalk.RevFlag;
106 : import org.eclipse.jgit.revwalk.RevSort;
107 : import org.eclipse.jgit.revwalk.RevWalk;
108 : import org.eclipse.jgit.util.TemporaryBuffer;
109 :
110 : /**
111 : * Utility methods used during the merge process.
112 : *
113 : * <p><strong>Note:</strong> Unless otherwise specified, the methods in this class <strong>do
114 : * not</strong> flush {@link ObjectInserter}s. Callers that want to read back objects before
115 : * flushing should use {@link ObjectInserter#newReader()}. This is already the default behavior of
116 : * {@code BatchUpdate}.
117 : */
118 : @AutoFactory
119 : public class MergeUtil {
120 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
121 :
122 : /**
123 : * Length of abbreviated hex SHA-1s in merged filenames.
124 : *
125 : * <p>This is a constant so output is stable over time even if the SHA-1 prefix becomes ambiguous.
126 : */
127 : private static final int NAME_ABBREV_LEN = 6;
128 :
129 : private static final String R_HEADS_MASTER = Constants.R_HEADS + Constants.MASTER;
130 :
131 : public static boolean useRecursiveMerge(Config cfg) {
132 152 : return cfg.getBoolean("core", null, "useRecursiveMerge", true);
133 : }
134 :
135 : public static ThreeWayMergeStrategy getMergeStrategy(Config cfg) {
136 152 : return useRecursiveMerge(cfg) ? MergeStrategy.RECURSIVE : MergeStrategy.RESOLVE;
137 : }
138 :
139 : private final IdentifiedUser.GenericFactory identifiedUserFactory;
140 : private final DynamicItem<UrlFormatter> urlFormatter;
141 : private final ApprovalsUtil approvalsUtil;
142 : private final ProjectState project;
143 : private final boolean useContentMerge;
144 : private final boolean useRecursiveMerge;
145 : private final PluggableCommitMessageGenerator commitMessageGenerator;
146 :
147 : MergeUtil(
148 : @Provided @GerritServerConfig Config serverConfig,
149 : @Provided IdentifiedUser.GenericFactory identifiedUserFactory,
150 : @Provided DynamicItem<UrlFormatter> urlFormatter,
151 : @Provided ApprovalsUtil approvalsUtil,
152 : @Provided PluggableCommitMessageGenerator commitMessageGenerator,
153 : ProjectState project) {
154 103 : this(
155 : serverConfig,
156 : identifiedUserFactory,
157 : urlFormatter,
158 : approvalsUtil,
159 : commitMessageGenerator,
160 : project,
161 103 : project.is(BooleanProjectConfig.USE_CONTENT_MERGE));
162 103 : }
163 :
164 : MergeUtil(
165 : @Provided @GerritServerConfig Config serverConfig,
166 : @Provided IdentifiedUser.GenericFactory identifiedUserFactory,
167 : @Provided DynamicItem<UrlFormatter> urlFormatter,
168 : @Provided ApprovalsUtil approvalsUtil,
169 : @Provided PluggableCommitMessageGenerator commitMessageGenerator,
170 : ProjectState project,
171 103 : boolean useContentMerge) {
172 103 : this.identifiedUserFactory = identifiedUserFactory;
173 103 : this.urlFormatter = urlFormatter;
174 103 : this.approvalsUtil = approvalsUtil;
175 103 : this.commitMessageGenerator = commitMessageGenerator;
176 103 : this.project = project;
177 103 : this.useContentMerge = useContentMerge;
178 103 : this.useRecursiveMerge = useRecursiveMerge(serverConfig);
179 103 : }
180 :
181 : public CodeReviewCommit getFirstFastForward(
182 : CodeReviewCommit mergeTip, RevWalk rw, List<CodeReviewCommit> toMerge) {
183 53 : for (Iterator<CodeReviewCommit> i = toMerge.iterator(); i.hasNext(); ) {
184 : try {
185 53 : final CodeReviewCommit n = i.next();
186 53 : if (mergeTip == null || rw.isMergedInto(mergeTip, n)) {
187 53 : i.remove();
188 53 : return n;
189 : }
190 0 : } catch (IOException e) {
191 0 : throw new StorageException("Cannot fast-forward test during merge", e);
192 14 : }
193 : }
194 14 : return mergeTip;
195 : }
196 :
197 : public List<CodeReviewCommit> reduceToMinimalMerge(
198 : MergeSorter mergeSorter, Collection<CodeReviewCommit> toSort) {
199 53 : List<CodeReviewCommit> result = new ArrayList<>();
200 : try {
201 53 : result.addAll(mergeSorter.sort(toSort));
202 0 : } catch (IOException | StorageException e) {
203 0 : throw new StorageException("Branch head sorting failed", e);
204 53 : }
205 53 : result.sort(CodeReviewCommit.ORDER);
206 53 : return result;
207 : }
208 :
209 : public CodeReviewCommit createCherryPickFromCommit(
210 : ObjectInserter inserter,
211 : Config repoConfig,
212 : RevCommit mergeTip,
213 : RevCommit originalCommit,
214 : PersonIdent cherryPickCommitterIdent,
215 : String commitMsg,
216 : CodeReviewRevWalk rw,
217 : int parentIndex,
218 : boolean ignoreIdenticalTree,
219 : boolean allowConflicts)
220 : throws IOException, MergeIdenticalTreeException, MergeConflictException,
221 : MethodNotAllowedException, InvalidMergeStrategyException {
222 :
223 14 : ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
224 14 : m.setBase(originalCommit.getParent(parentIndex));
225 :
226 14 : DirCache dc = DirCache.newInCore();
227 14 : if (allowConflicts && m instanceof ResolveMerger) {
228 : // The DirCache must be set on ResolveMerger before calling
229 : // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
230 3 : ((ResolveMerger) m).setDirCache(dc);
231 : }
232 :
233 : ObjectId tree;
234 : ImmutableSet<String> filesWithGitConflicts;
235 14 : if (m.merge(mergeTip, originalCommit)) {
236 13 : filesWithGitConflicts = null;
237 13 : tree = m.getResultTreeId();
238 13 : if (tree.equals(mergeTip.getTree()) && !ignoreIdenticalTree) {
239 3 : throw new MergeIdenticalTreeException("identical tree");
240 : }
241 : } else {
242 3 : if (!allowConflicts) {
243 2 : throw new MergeConflictException(
244 2 : String.format(
245 : "merge conflict while merging commits %s and %s",
246 2 : mergeTip.toObjectId(), originalCommit.toObjectId()));
247 : }
248 :
249 2 : if (!useContentMerge) {
250 : // If content merge is disabled we don't have a ResolveMerger and hence cannot merge with
251 : // conflict markers.
252 0 : throw new MethodNotAllowedException(
253 : "Cherry-pick with allow conflicts requires that content merge is enabled.");
254 : }
255 :
256 : // For merging with conflict markers we need a ResolveMerger, double-check that we have one.
257 2 : checkState(m instanceof ResolveMerger, "allow conflicts is not supported");
258 2 : Map<String, MergeResult<? extends Sequence>> mergeResults =
259 2 : ((ResolveMerger) m).getMergeResults();
260 :
261 2 : filesWithGitConflicts =
262 2 : mergeResults.entrySet().stream()
263 2 : .filter(e -> e.getValue().containsConflicts())
264 2 : .map(Map.Entry::getKey)
265 2 : .collect(toImmutableSet());
266 :
267 2 : tree =
268 2 : mergeWithConflicts(
269 : rw, inserter, dc, "HEAD", mergeTip, "CHANGE", originalCommit, mergeResults);
270 : }
271 :
272 14 : CommitBuilder cherryPickCommit = new CommitBuilder();
273 14 : cherryPickCommit.setTreeId(tree);
274 14 : cherryPickCommit.setParentId(mergeTip);
275 14 : cherryPickCommit.setAuthor(originalCommit.getAuthorIdent());
276 14 : cherryPickCommit.setCommitter(cherryPickCommitterIdent);
277 14 : cherryPickCommit.setMessage(commitMsg);
278 14 : matchAuthorToCommitterDate(project, cherryPickCommit);
279 14 : CodeReviewCommit commit = rw.parseCommit(inserter.insert(cherryPickCommit));
280 14 : commit.setFilesWithGitConflicts(filesWithGitConflicts);
281 14 : return commit;
282 : }
283 :
284 : @SuppressWarnings("resource") // TemporaryBuffer requires calling close before reading.
285 : public static ObjectId mergeWithConflicts(
286 : RevWalk rw,
287 : ObjectInserter ins,
288 : DirCache dc,
289 : String oursName,
290 : RevCommit ours,
291 : String theirsName,
292 : RevCommit theirs,
293 : Map<String, MergeResult<? extends Sequence>> mergeResults)
294 : throws IOException {
295 26 : rw.parseBody(ours);
296 26 : rw.parseBody(theirs);
297 26 : String oursMsg = ours.getShortMessage();
298 26 : String theirsMsg = theirs.getShortMessage();
299 :
300 26 : int nameLength = Math.max(oursName.length(), theirsName.length());
301 26 : String oursNameFormatted =
302 26 : String.format(
303 : "%-" + nameLength + "s (%s %s)",
304 : oursName,
305 26 : abbreviateName(ours, NAME_ABBREV_LEN),
306 26 : oursMsg.substring(0, Math.min(oursMsg.length(), 60)));
307 26 : String theirsNameFormatted =
308 26 : String.format(
309 : "%-" + nameLength + "s (%s %s)",
310 : theirsName,
311 26 : abbreviateName(theirs, NAME_ABBREV_LEN),
312 26 : theirsMsg.substring(0, Math.min(theirsMsg.length(), 60)));
313 :
314 26 : MergeFormatter fmt = new MergeFormatter();
315 26 : Map<String, ObjectId> resolved = new HashMap<>();
316 26 : for (Map.Entry<String, MergeResult<? extends Sequence>> entry : mergeResults.entrySet()) {
317 26 : MergeResult<? extends Sequence> p = entry.getValue();
318 26 : TemporaryBuffer buf = null;
319 : try {
320 : // TODO(dborowitz): Respect inCoreLimit here.
321 26 : buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024);
322 26 : fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
323 26 : buf.close(); // Flush file and close for writes, but leave available for reading.
324 :
325 26 : try (InputStream in = buf.openInputStream()) {
326 26 : resolved.put(entry.getKey(), ins.insert(Constants.OBJ_BLOB, buf.length(), in));
327 : }
328 : } finally {
329 26 : if (buf != null) {
330 26 : buf.destroy();
331 : }
332 : }
333 26 : }
334 :
335 26 : DirCacheBuilder builder = dc.builder();
336 26 : int cnt = dc.getEntryCount();
337 26 : for (int i = 0; i < cnt; ) {
338 26 : DirCacheEntry entry = dc.getEntry(i);
339 26 : if (entry.getStage() == 0) {
340 7 : builder.add(entry);
341 7 : i++;
342 7 : continue;
343 : }
344 :
345 26 : int next = dc.nextEntry(i);
346 26 : String path = entry.getPathString();
347 26 : DirCacheEntry res = new DirCacheEntry(path);
348 26 : if (resolved.containsKey(path)) {
349 : // For a file with content merge conflict that we produced a result
350 : // above on, collapse the file down to a single stage 0 with just
351 : // the blob content, and a randomly selected mode (the lowest stage,
352 : // which should be the merge base, or ours).
353 26 : res.setFileMode(entry.getFileMode());
354 26 : res.setObjectId(resolved.get(path));
355 :
356 0 : } else if (next == i + 1) {
357 : // If there is exactly one stage present, shouldn't be a conflict...
358 0 : res.setFileMode(entry.getFileMode());
359 0 : res.setObjectId(entry.getObjectId());
360 :
361 0 : } else if (next == i + 2) {
362 : // Two stages suggests a delete/modify conflict. Pick the higher
363 : // stage as the automatic result.
364 0 : entry = dc.getEntry(i + 1);
365 0 : res.setFileMode(entry.getFileMode());
366 0 : res.setObjectId(entry.getObjectId());
367 :
368 : } else {
369 : // 3 stage conflict, no resolve above
370 : // Punt on the 3-stage conflict and show the base, for now.
371 0 : res.setFileMode(entry.getFileMode());
372 0 : res.setObjectId(entry.getObjectId());
373 : }
374 26 : builder.add(res);
375 26 : i = next;
376 26 : }
377 26 : builder.finish();
378 26 : return dc.writeTree(ins);
379 : }
380 :
381 : public static CodeReviewCommit createMergeCommit(
382 : ObjectInserter inserter,
383 : Config repoConfig,
384 : RevCommit mergeTip,
385 : RevCommit originalCommit,
386 : String mergeStrategy,
387 : boolean allowConflicts,
388 : PersonIdent committerIdent,
389 : String commitMsg,
390 : CodeReviewRevWalk rw)
391 : throws IOException, MergeIdenticalTreeException, MergeConflictException,
392 : InvalidMergeStrategyException {
393 1 : return createMergeCommit(
394 : inserter,
395 : repoConfig,
396 : mergeTip,
397 : originalCommit,
398 : mergeStrategy,
399 : allowConflicts,
400 : committerIdent,
401 : committerIdent,
402 : commitMsg,
403 : rw);
404 : }
405 :
406 : public static CodeReviewCommit createMergeCommit(
407 : ObjectInserter inserter,
408 : Config repoConfig,
409 : RevCommit mergeTip,
410 : RevCommit originalCommit,
411 : String mergeStrategy,
412 : boolean allowConflicts,
413 : PersonIdent authorIdent,
414 : PersonIdent committerIdent,
415 : String commitMsg,
416 : CodeReviewRevWalk rw)
417 : throws IOException, MergeIdenticalTreeException, MergeConflictException,
418 : InvalidMergeStrategyException {
419 :
420 3 : if (!MergeStrategy.THEIRS.getName().equals(mergeStrategy)
421 3 : && rw.isMergedInto(originalCommit, mergeTip)) {
422 1 : throw new ChangeAlreadyMergedException(
423 1 : "'" + originalCommit.getName() + "' has already been merged");
424 : }
425 :
426 3 : Merger m = newMerger(inserter, repoConfig, mergeStrategy);
427 :
428 3 : DirCache dc = DirCache.newInCore();
429 3 : if (allowConflicts && m instanceof ResolveMerger) {
430 : // The DirCache must be set on ResolveMerger before calling
431 : // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
432 2 : ((ResolveMerger) m).setDirCache(dc);
433 : }
434 :
435 : ObjectId tree;
436 : ImmutableSet<String> filesWithGitConflicts;
437 3 : if (m.merge(false, mergeTip, originalCommit)) {
438 3 : filesWithGitConflicts = null;
439 3 : tree = m.getResultTreeId();
440 : } else {
441 2 : List<String> conflicts = ImmutableList.of();
442 2 : if (m instanceof ResolveMerger) {
443 2 : conflicts = ((ResolveMerger) m).getUnmergedPaths();
444 : }
445 :
446 2 : if (!allowConflicts) {
447 2 : throw new MergeConflictException(createConflictMessage(conflicts));
448 : }
449 :
450 : // For merging with conflict markers we need a ResolveMerger, double-check that we have one.
451 2 : if (!(m instanceof ResolveMerger)) {
452 2 : throw new MergeWithConflictsNotSupportedException(MergeStrategy.get(mergeStrategy));
453 : }
454 2 : Map<String, MergeResult<? extends Sequence>> mergeResults =
455 2 : ((ResolveMerger) m).getMergeResults();
456 :
457 2 : filesWithGitConflicts =
458 2 : mergeResults.entrySet().stream()
459 2 : .filter(e -> e.getValue().containsConflicts())
460 2 : .map(Map.Entry::getKey)
461 2 : .collect(toImmutableSet());
462 :
463 2 : tree =
464 2 : mergeWithConflicts(
465 : rw,
466 : inserter,
467 : dc,
468 : "TARGET BRANCH",
469 : mergeTip,
470 : "SOURCE BRANCH",
471 : originalCommit,
472 : mergeResults);
473 : }
474 :
475 3 : CommitBuilder mergeCommit = new CommitBuilder();
476 3 : mergeCommit.setTreeId(tree);
477 3 : mergeCommit.setParentIds(mergeTip, originalCommit);
478 3 : mergeCommit.setAuthor(authorIdent);
479 3 : mergeCommit.setCommitter(committerIdent);
480 3 : mergeCommit.setMessage(commitMsg);
481 3 : CodeReviewCommit commit = rw.parseCommit(inserter.insert(mergeCommit));
482 3 : commit.setFilesWithGitConflicts(filesWithGitConflicts);
483 3 : return commit;
484 : }
485 :
486 : public static String createConflictMessage(List<String> conflicts) {
487 5 : if (conflicts.isEmpty()) {
488 0 : return "";
489 : }
490 :
491 5 : StringBuilder sb = new StringBuilder("merge conflict(s):");
492 5 : for (String c : conflicts) {
493 5 : sb.append('\n').append(c);
494 5 : }
495 5 : return sb.toString();
496 : }
497 :
498 : /**
499 : * Adds footers to existing commit message based on the state of the change.
500 : *
501 : * <p>This adds the following footers if they are missing:
502 : *
503 : * <ul>
504 : * <li>Reviewed-on: <i>url</i>
505 : * <li>Reviewed-by | Tested-by | <i>Other-Label-Name</i>: <i>reviewer</i>
506 : * <li>Change-Id
507 : * </ul>
508 : *
509 : * @return new message
510 : */
511 : private String createDetailedCommitMessage(RevCommit n, ChangeNotes notes, PatchSet.Id psId) {
512 103 : Change c = notes.getChange();
513 103 : final List<FooterLine> footers = n.getFooterLines();
514 103 : final StringBuilder msgbuf = new StringBuilder();
515 103 : msgbuf.append(n.getFullMessage());
516 :
517 103 : if (msgbuf.length() == 0) {
518 : // WTF, an empty commit message?
519 4 : msgbuf.append("<no commit message provided>");
520 : }
521 103 : if (msgbuf.charAt(msgbuf.length() - 1) != '\n') {
522 : // Missing a trailing LF? Correct it (perhaps the editor was broken).
523 10 : msgbuf.append('\n');
524 : }
525 103 : if (footers.isEmpty()) {
526 : // Doesn't end in a "Signed-off-by: ..." style line? Add another line
527 : // break to start a new paragraph for the reviewed-by tag lines.
528 : //
529 10 : msgbuf.append('\n');
530 : }
531 :
532 103 : if (ChangeUtil.getChangeIdsFromFooter(n, urlFormatter.get()).isEmpty()) {
533 10 : msgbuf.append(FooterConstants.CHANGE_ID.getName());
534 10 : msgbuf.append(": ");
535 10 : msgbuf.append(c.getKey().get());
536 10 : msgbuf.append('\n');
537 : }
538 :
539 103 : Optional<String> url = urlFormatter.get().getChangeViewUrl(c.getProject(), c.getId());
540 103 : if (url.isPresent()) {
541 103 : if (!contains(footers, FooterConstants.REVIEWED_ON, url.get())) {
542 103 : msgbuf
543 103 : .append(FooterConstants.REVIEWED_ON.getName())
544 103 : .append(": ")
545 103 : .append(url.get())
546 103 : .append('\n');
547 : }
548 : }
549 103 : PatchSetApproval submitAudit = null;
550 :
551 103 : for (PatchSetApproval a : safeGetApprovals(notes, psId)) {
552 67 : if (a.value() <= 0) {
553 : // Negative votes aren't counted.
554 23 : continue;
555 : }
556 :
557 67 : if (a.isLegacySubmit()) {
558 : // Submit is treated specially, below (becomes committer)
559 : //
560 55 : if (submitAudit == null || a.granted().compareTo(submitAudit.granted()) > 0) {
561 55 : submitAudit = a;
562 : }
563 : continue;
564 : }
565 :
566 58 : final Account acc = identifiedUserFactory.create(a.accountId()).getAccount();
567 58 : final StringBuilder identbuf = new StringBuilder();
568 58 : if (acc.fullName() != null && acc.fullName().length() > 0) {
569 54 : if (identbuf.length() > 0) {
570 0 : identbuf.append(' ');
571 : }
572 54 : identbuf.append(acc.fullName());
573 : }
574 58 : if (acc.preferredEmail() != null && acc.preferredEmail().length() > 0) {
575 58 : if (isSignedOffBy(footers, acc.preferredEmail())) {
576 0 : continue;
577 : }
578 58 : if (identbuf.length() > 0) {
579 54 : identbuf.append(' ');
580 : }
581 58 : identbuf.append('<');
582 58 : identbuf.append(acc.preferredEmail());
583 58 : identbuf.append('>');
584 : }
585 58 : if (identbuf.length() == 0) {
586 : // Nothing reasonable to describe them by? Ignore them.
587 6 : continue;
588 : }
589 :
590 : final String tag;
591 58 : if (isCodeReview(a.labelId())) {
592 56 : tag = "Reviewed-by";
593 17 : } else if (isVerified(a.labelId())) {
594 11 : tag = "Tested-by";
595 : } else {
596 9 : final Optional<LabelType> lt = project.getLabelTypes().byLabel(a.labelId());
597 9 : if (!lt.isPresent()) {
598 0 : continue;
599 : }
600 9 : tag = lt.get().getName();
601 : }
602 :
603 58 : if (!contains(footers, new FooterKey(tag), identbuf.toString())) {
604 58 : msgbuf.append(tag);
605 58 : msgbuf.append(": ");
606 58 : msgbuf.append(identbuf);
607 58 : msgbuf.append('\n');
608 : }
609 58 : }
610 103 : return msgbuf.toString();
611 : }
612 :
613 : public String createCommitMessageOnSubmit(CodeReviewCommit n, RevCommit mergeTip) {
614 8 : return createCommitMessageOnSubmit(n, mergeTip, n.notes(), n.getPatchsetId());
615 : }
616 :
617 : /**
618 : * Creates a commit message for a change, which can be customized by plugins.
619 : *
620 : * <p>By default, adds footers to existing commit message based on the state of the change.
621 : * Plugins implementing {@link ChangeMessageModifier} can modify the resulting commit message
622 : * arbitrarily.
623 : *
624 : * @return new message
625 : */
626 : public String createCommitMessageOnSubmit(
627 : RevCommit n, @Nullable RevCommit mergeTip, ChangeNotes notes, PatchSet.Id id) {
628 103 : return commitMessageGenerator.generate(
629 103 : n, mergeTip, notes.getChange().getDest(), createDetailedCommitMessage(n, notes, id));
630 : }
631 :
632 : private static boolean isCodeReview(LabelId id) {
633 58 : return LabelId.CODE_REVIEW.equalsIgnoreCase(id.get());
634 : }
635 :
636 : private static boolean isVerified(LabelId id) {
637 17 : return LabelId.VERIFIED.equalsIgnoreCase(id.get());
638 : }
639 :
640 : private Iterable<PatchSetApproval> safeGetApprovals(ChangeNotes notes, PatchSet.Id psId) {
641 : try {
642 103 : return approvalsUtil.byPatchSet(notes, psId);
643 0 : } catch (StorageException e) {
644 0 : logger.atSevere().withCause(e).log("Can't read approval records for %s", psId);
645 0 : return Collections.emptyList();
646 : }
647 : }
648 :
649 : private static boolean contains(List<FooterLine> footers, FooterKey key, String val) {
650 103 : for (FooterLine line : footers) {
651 103 : if (line.matches(key) && val.equals(line.getValue())) {
652 8 : return true;
653 : }
654 103 : }
655 103 : return false;
656 : }
657 :
658 : private static boolean isSignedOffBy(List<FooterLine> footers, String email) {
659 58 : for (FooterLine line : footers) {
660 58 : if (line.matches(FooterKey.SIGNED_OFF_BY) && email.equals(line.getEmailAddress())) {
661 0 : return true;
662 : }
663 58 : }
664 58 : return false;
665 : }
666 :
667 : public boolean canMerge(
668 : MergeSorter mergeSorter,
669 : Repository repo,
670 : CodeReviewCommit mergeTip,
671 : CodeReviewCommit toMerge) {
672 13 : if (hasMissingDependencies(mergeSorter, toMerge)) {
673 0 : return false;
674 : }
675 :
676 13 : try (ObjectInserter ins = new InMemoryInserter(repo)) {
677 13 : return newThreeWayMerger(ins, repo.getConfig()).merge(mergeTip, toMerge);
678 0 : } catch (LargeObjectException e) {
679 0 : logger.atWarning().log("Cannot merge due to LargeObjectException: %s", toMerge.name());
680 0 : return false;
681 0 : } catch (NoMergeBaseException e) {
682 0 : return false;
683 0 : } catch (IOException e) {
684 0 : throw new StorageException("Cannot merge " + toMerge.name(), e);
685 : }
686 : }
687 :
688 : public boolean canFastForward(
689 : MergeSorter mergeSorter,
690 : CodeReviewCommit mergeTip,
691 : CodeReviewRevWalk rw,
692 : CodeReviewCommit toMerge) {
693 23 : if (hasMissingDependencies(mergeSorter, toMerge)) {
694 2 : return false;
695 : }
696 :
697 : try {
698 23 : return mergeTip == null
699 23 : || rw.isMergedInto(mergeTip, toMerge)
700 23 : || rw.isMergedInto(toMerge, mergeTip);
701 0 : } catch (IOException e) {
702 0 : throw new StorageException("Cannot fast-forward test during merge", e);
703 : }
704 : }
705 :
706 : public boolean canCherryPick(
707 : MergeSorter mergeSorter,
708 : Repository repo,
709 : CodeReviewCommit mergeTip,
710 : CodeReviewRevWalk rw,
711 : CodeReviewCommit toMerge) {
712 3 : if (mergeTip == null) {
713 : // The branch is unborn. Fast-forward is possible.
714 : //
715 0 : return true;
716 : }
717 :
718 3 : if (toMerge.getParentCount() == 0) {
719 : // Refuse to merge a root commit into an existing branch,
720 : // we cannot obtain a delta for the cherry-pick to apply.
721 : //
722 0 : return false;
723 : }
724 :
725 3 : if (toMerge.getParentCount() == 1) {
726 : // If there is only one parent, a cherry-pick can be done by
727 : // taking the delta relative to that one parent and redoing
728 : // that on the current merge tip.
729 : //
730 3 : try (ObjectInserter ins = new InMemoryInserter(repo)) {
731 3 : ThreeWayMerger m = newThreeWayMerger(ins, repo.getConfig());
732 3 : m.setBase(toMerge.getParent(0));
733 3 : return m.merge(mergeTip, toMerge);
734 0 : } catch (IOException e) {
735 0 : throw new StorageException(
736 0 : String.format(
737 0 : "Cannot merge commit %s with mergetip %s", toMerge.name(), mergeTip.name()),
738 : e);
739 : }
740 : }
741 :
742 : // There are multiple parents, so this is a merge commit. We
743 : // don't want to cherry-pick the merge as clients can't easily
744 : // rebase their history with that merge present and replaced
745 : // by an equivalent merge with a different first parent. So
746 : // instead behave as though MERGE_IF_NECESSARY was configured.
747 : //
748 1 : return canFastForward(mergeSorter, mergeTip, rw, toMerge)
749 1 : || canMerge(mergeSorter, repo, mergeTip, toMerge);
750 : }
751 :
752 : public boolean hasMissingDependencies(MergeSorter mergeSorter, CodeReviewCommit toMerge) {
753 : try {
754 23 : return !mergeSorter.sort(Collections.singleton(toMerge)).contains(toMerge);
755 0 : } catch (IOException | StorageException e) {
756 0 : throw new StorageException("Branch head sorting failed", e);
757 : }
758 : }
759 :
760 : public CodeReviewCommit mergeOneCommit(
761 : PersonIdent author,
762 : PersonIdent committer,
763 : CodeReviewRevWalk rw,
764 : ObjectInserter inserter,
765 : Config repoConfig,
766 : BranchNameKey destBranch,
767 : CodeReviewCommit mergeTip,
768 : CodeReviewCommit n)
769 : throws InvalidMergeStrategyException {
770 20 : ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
771 : try {
772 20 : if (m.merge(mergeTip, n)) {
773 20 : return writeMergeCommit(
774 20 : author, committer, rw, inserter, destBranch, mergeTip, m.getResultTreeId(), n);
775 : }
776 4 : failed(rw, mergeTip, n, CommitMergeStatus.PATH_CONFLICT);
777 0 : } catch (NoMergeBaseException e) {
778 : try {
779 0 : failed(rw, mergeTip, n, getCommitMergeStatus(e.getReason()));
780 0 : } catch (IOException e2) {
781 0 : throw new StorageException("Cannot merge " + n.name(), e2);
782 0 : }
783 0 : } catch (IOException e) {
784 0 : throw new StorageException("Cannot merge " + n.name(), e);
785 4 : }
786 4 : return mergeTip;
787 : }
788 :
789 : private static CommitMergeStatus getCommitMergeStatus(MergeBaseFailureReason reason) {
790 0 : switch (reason) {
791 : case MULTIPLE_MERGE_BASES_NOT_SUPPORTED:
792 : case TOO_MANY_MERGE_BASES:
793 : default:
794 0 : return CommitMergeStatus.MANUAL_RECURSIVE_MERGE;
795 : case CONFLICTS_DURING_MERGE_BASE_CALCULATION:
796 0 : return CommitMergeStatus.PATH_CONFLICT;
797 : }
798 : }
799 :
800 : private static CodeReviewCommit failed(
801 : CodeReviewRevWalk rw,
802 : CodeReviewCommit mergeTip,
803 : CodeReviewCommit n,
804 : CommitMergeStatus failure)
805 : throws MissingObjectException, IncorrectObjectTypeException, IOException {
806 4 : rw.reset();
807 4 : rw.markStart(n);
808 4 : rw.markUninteresting(mergeTip);
809 : CodeReviewCommit failed;
810 4 : while ((failed = rw.next()) != null) {
811 4 : failed.setStatusCode(failure);
812 : }
813 4 : return failed;
814 : }
815 :
816 : public CodeReviewCommit writeMergeCommit(
817 : PersonIdent author,
818 : PersonIdent committer,
819 : CodeReviewRevWalk rw,
820 : ObjectInserter inserter,
821 : BranchNameKey destBranch,
822 : CodeReviewCommit mergeTip,
823 : ObjectId treeId,
824 : CodeReviewCommit n)
825 : throws IOException, MissingObjectException, IncorrectObjectTypeException {
826 20 : final List<CodeReviewCommit> merged = new ArrayList<>();
827 20 : rw.reset();
828 20 : rw.markStart(n);
829 20 : rw.markUninteresting(mergeTip);
830 : CodeReviewCommit crc;
831 20 : while ((crc = rw.next()) != null) {
832 20 : if (crc.getPatchsetId() != null) {
833 20 : merged.add(crc);
834 : }
835 : }
836 :
837 20 : StringBuilder msgbuf = new StringBuilder().append(summarize(rw, merged));
838 20 : if (!R_HEADS_MASTER.equals(destBranch.branch())) {
839 3 : msgbuf.append(" into ");
840 3 : msgbuf.append(destBranch.shortName());
841 : }
842 :
843 20 : if (merged.size() > 1) {
844 5 : msgbuf.append("\n\n* changes:\n");
845 5 : for (CodeReviewCommit c : merged) {
846 5 : rw.parseBody(c);
847 5 : msgbuf.append(" ");
848 5 : msgbuf.append(c.getShortMessage());
849 5 : msgbuf.append("\n");
850 5 : }
851 : }
852 :
853 20 : final CommitBuilder mergeCommit = new CommitBuilder();
854 20 : mergeCommit.setTreeId(treeId);
855 20 : mergeCommit.setParentIds(mergeTip, n);
856 20 : mergeCommit.setAuthor(author);
857 20 : mergeCommit.setCommitter(committer);
858 20 : mergeCommit.setMessage(msgbuf.toString());
859 :
860 20 : CodeReviewCommit mergeResult = rw.parseCommit(inserter.insert(mergeCommit));
861 20 : mergeResult.setNotes(n.getNotes());
862 20 : return mergeResult;
863 : }
864 :
865 : private String summarize(RevWalk rw, List<CodeReviewCommit> merged) throws IOException {
866 20 : if (merged.size() == 1) {
867 20 : CodeReviewCommit c = merged.get(0);
868 20 : rw.parseBody(c);
869 20 : return String.format("Merge \"%s\"", c.getShortMessage());
870 : }
871 :
872 5 : ImmutableSortedSet<String> topics =
873 5 : merged.stream()
874 5 : .map(c -> c.change().getTopic())
875 5 : .filter(t -> !Strings.isNullOrEmpty(t))
876 5 : .map(t -> "\"" + t + "\"")
877 5 : .collect(toImmutableSortedSet(naturalOrder()));
878 :
879 5 : if (!topics.isEmpty()) {
880 2 : return String.format(
881 : "Merge changes from topic%s %s",
882 2 : topics.size() > 1 ? "s" : "", topics.stream().collect(joining(", ")));
883 : }
884 4 : return merged.stream()
885 4 : .limit(5)
886 4 : .map(c -> c.change().getKey().abbreviate())
887 4 : .collect(joining(",", "Merge changes ", merged.size() > 5 ? ", ..." : ""));
888 : }
889 :
890 : public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig)
891 : throws InvalidMergeStrategyException {
892 36 : return newThreeWayMerger(inserter, repoConfig, mergeStrategyName());
893 : }
894 :
895 : public String mergeStrategyName() {
896 40 : return mergeStrategyName(useContentMerge, useRecursiveMerge);
897 : }
898 :
899 : public static String mergeStrategyName(boolean useContentMerge, boolean useRecursiveMerge) {
900 : String mergeStrategy;
901 :
902 62 : if (useContentMerge) {
903 : // Settings for this project allow us to try and automatically resolve
904 : // conflicts within files if needed. Use either the old resolve merger or
905 : // new recursive merger, and instruct to operate in core.
906 62 : if (useRecursiveMerge) {
907 62 : mergeStrategy = MergeStrategy.RECURSIVE.getName();
908 : } else {
909 0 : mergeStrategy = MergeStrategy.RESOLVE.getName();
910 : }
911 : } else {
912 : // No auto conflict resolving allowed. If any of the
913 : // affected files was modified, merge will fail.
914 6 : mergeStrategy = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.getName();
915 : }
916 :
917 62 : logger.atFine().log(
918 : "mergeStrategy = %s (useContentMerge = %s, useRecursiveMerge = %s)",
919 62 : mergeStrategy, useContentMerge, useRecursiveMerge);
920 62 : return mergeStrategy;
921 : }
922 :
923 : public static ThreeWayMerger newThreeWayMerger(
924 : ObjectInserter inserter, Config repoConfig, String strategyName)
925 : throws InvalidMergeStrategyException {
926 54 : Merger m = newMerger(inserter, repoConfig, strategyName);
927 54 : checkArgument(
928 : m instanceof ThreeWayMerger,
929 : "merge strategy %s does not support three-way merging",
930 : strategyName);
931 54 : return (ThreeWayMerger) m;
932 : }
933 :
934 : public static Merger newMerger(ObjectInserter inserter, Config repoConfig, String strategyName)
935 : throws InvalidMergeStrategyException {
936 56 : MergeStrategy strategy = MergeStrategy.get(strategyName);
937 56 : if (strategy == null) {
938 3 : throw new InvalidMergeStrategyException(strategyName);
939 : }
940 56 : return strategy.newMerger(
941 56 : new ObjectInserter.Filter() {
942 : @Override
943 : protected ObjectInserter delegate() {
944 56 : return inserter;
945 : }
946 :
947 : @Override
948 53 : public void flush() {}
949 :
950 : @Override
951 54 : public void close() {}
952 : },
953 : repoConfig);
954 : }
955 :
956 : public void markCleanMerges(
957 : RevWalk rw, RevFlag canMergeFlag, CodeReviewCommit mergeTip, Set<RevCommit> alreadyAccepted) {
958 53 : if (mergeTip == null) {
959 : // If mergeTip is null here, branchTip was null, indicating a new branch
960 : // at the start of the merge process. We also elected to merge nothing,
961 : // probably due to missing dependencies. Nothing was cleanly merged.
962 : //
963 0 : return;
964 : }
965 :
966 : try {
967 53 : rw.resetRetain(canMergeFlag);
968 53 : rw.sort(RevSort.TOPO);
969 53 : rw.sort(RevSort.REVERSE, true);
970 53 : rw.markStart(mergeTip);
971 53 : for (RevCommit c : alreadyAccepted) {
972 : // If branch was not created by this submit.
973 53 : if (!Objects.equals(c, mergeTip)) {
974 53 : rw.markUninteresting(c);
975 : }
976 53 : }
977 :
978 : CodeReviewCommit c;
979 53 : while ((c = (CodeReviewCommit) rw.next()) != null) {
980 53 : if (c.getPatchsetId() != null && c.getStatusCode() == null) {
981 53 : c.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
982 : }
983 : }
984 0 : } catch (IOException e) {
985 0 : throw new StorageException("Cannot mark clean merges", e);
986 53 : }
987 53 : }
988 :
989 : public Set<Change.Id> findUnmergedChanges(
990 : Set<Change.Id> expected,
991 : CodeReviewRevWalk rw,
992 : RevFlag canMergeFlag,
993 : CodeReviewCommit oldTip,
994 : CodeReviewCommit mergeTip,
995 : Iterable<Change.Id> alreadyMerged) {
996 53 : if (mergeTip == null) {
997 0 : return expected;
998 : }
999 :
1000 : try {
1001 53 : Set<Change.Id> found = Sets.newHashSetWithExpectedSize(expected.size());
1002 53 : Iterables.addAll(found, alreadyMerged);
1003 53 : rw.resetRetain(canMergeFlag);
1004 53 : rw.sort(RevSort.TOPO);
1005 53 : rw.markStart(mergeTip);
1006 53 : if (oldTip != null) {
1007 53 : rw.markUninteresting(oldTip);
1008 : }
1009 :
1010 : CodeReviewCommit c;
1011 53 : while ((c = rw.next()) != null) {
1012 53 : if (c.getPatchsetId() == null) {
1013 20 : continue;
1014 : }
1015 53 : Change.Id id = c.getPatchsetId().changeId();
1016 53 : if (!expected.contains(id)) {
1017 0 : continue;
1018 : }
1019 53 : found.add(id);
1020 53 : if (found.size() == expected.size()) {
1021 53 : return Collections.emptySet();
1022 : }
1023 15 : }
1024 6 : return Sets.difference(expected, found);
1025 0 : } catch (IOException e) {
1026 0 : throw new StorageException("Cannot check if changes were merged", e);
1027 : }
1028 : }
1029 :
1030 : @Nullable
1031 : public static CodeReviewCommit findAnyMergedInto(
1032 : CodeReviewRevWalk rw, Iterable<CodeReviewCommit> commits, CodeReviewCommit tip)
1033 : throws IOException {
1034 53 : for (CodeReviewCommit c : commits) {
1035 : // TODO(dborowitz): Seems like this could get expensive for many patch
1036 : // sets. Is there a more efficient implementation?
1037 53 : if (rw.isMergedInto(c, tip)) {
1038 7 : return c;
1039 : }
1040 53 : }
1041 53 : return null;
1042 : }
1043 :
1044 : public static RevCommit resolveCommit(Repository repo, RevWalk rw, String str)
1045 : throws BadRequestException, ResourceNotFoundException, IOException {
1046 : try {
1047 4 : ObjectId commitId = repo.resolve(str);
1048 4 : if (commitId == null) {
1049 2 : throw new BadRequestException("Cannot resolve '" + str + "' to a commit");
1050 : }
1051 4 : return rw.parseCommit(commitId);
1052 0 : } catch (AmbiguousObjectException | IncorrectObjectTypeException | RevisionSyntaxException e) {
1053 0 : throw new BadRequestException(e.getMessage());
1054 0 : } catch (MissingObjectException e) {
1055 0 : throw new ResourceNotFoundException(e.getMessage());
1056 : }
1057 : }
1058 :
1059 : private static void matchAuthorToCommitterDate(ProjectState project, CommitBuilder commit) {
1060 14 : if (project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE)) {
1061 2 : commit.setAuthor(
1062 : new PersonIdent(
1063 2 : commit.getAuthor(),
1064 2 : commit.getCommitter().getWhen(),
1065 2 : commit.getCommitter().getTimeZone()));
1066 : }
1067 14 : }
1068 : }
|