1
2
3
4
5
6
7
8
9
10
11
12 package org.eclipse.jgit.api;
13
14 import java.io.IOException;
15 import java.text.MessageFormat;
16 import java.util.Arrays;
17 import java.util.Collections;
18 import java.util.LinkedList;
19 import java.util.List;
20 import java.util.Locale;
21 import java.util.Map;
22
23 import org.eclipse.jgit.annotations.Nullable;
24 import org.eclipse.jgit.api.MergeResult.MergeStatus;
25 import org.eclipse.jgit.api.errors.CheckoutConflictException;
26 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
27 import org.eclipse.jgit.api.errors.GitAPIException;
28 import org.eclipse.jgit.api.errors.InvalidMergeHeadsException;
29 import org.eclipse.jgit.api.errors.JGitInternalException;
30 import org.eclipse.jgit.api.errors.NoHeadException;
31 import org.eclipse.jgit.api.errors.NoMessageException;
32 import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
33 import org.eclipse.jgit.dircache.DirCacheCheckout;
34 import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
35 import org.eclipse.jgit.internal.JGitText;
36 import org.eclipse.jgit.lib.AnyObjectId;
37 import org.eclipse.jgit.lib.Config.ConfigEnum;
38 import org.eclipse.jgit.lib.Constants;
39 import org.eclipse.jgit.lib.NullProgressMonitor;
40 import org.eclipse.jgit.lib.ObjectId;
41 import org.eclipse.jgit.lib.ObjectIdRef;
42 import org.eclipse.jgit.lib.ProgressMonitor;
43 import org.eclipse.jgit.lib.Ref;
44 import org.eclipse.jgit.lib.Ref.Storage;
45 import org.eclipse.jgit.lib.RefUpdate;
46 import org.eclipse.jgit.lib.RefUpdate.Result;
47 import org.eclipse.jgit.lib.Repository;
48 import org.eclipse.jgit.merge.ContentMergeStrategy;
49 import org.eclipse.jgit.merge.MergeConfig;
50 import org.eclipse.jgit.merge.MergeMessageFormatter;
51 import org.eclipse.jgit.merge.MergeStrategy;
52 import org.eclipse.jgit.merge.Merger;
53 import org.eclipse.jgit.merge.ResolveMerger;
54 import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
55 import org.eclipse.jgit.merge.SquashMessageFormatter;
56 import org.eclipse.jgit.revwalk.RevCommit;
57 import org.eclipse.jgit.revwalk.RevWalk;
58 import org.eclipse.jgit.revwalk.RevWalkUtils;
59 import org.eclipse.jgit.treewalk.FileTreeIterator;
60 import org.eclipse.jgit.util.StringUtils;
61
62
63
64
65
66
67
68
69
70
71 public class MergeCommand extends GitCommand<MergeResult> {
72
73 private MergeStrategy mergeStrategy = MergeStrategy.RECURSIVE;
74
75 private ContentMergeStrategy contentStrategy;
76
77 private List<Ref> commits = new LinkedList<>();
78
79 private Boolean squash;
80
81 private FastForwardMode fastForwardMode;
82
83 private String message;
84
85 private boolean insertChangeId;
86
87 private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
88
89
90
91
92
93
94 public enum ConflictStyle {
95
96
97 MERGE,
98
99
100 DIFF3
101 }
102
103
104
105
106
107
108 public enum FastForwardMode implements ConfigEnum {
109
110
111
112
113 FF,
114
115
116
117
118 NO_FF,
119
120
121
122
123 FF_ONLY;
124
125 @Override
126 public String toConfigValue() {
127 return "--" + name().toLowerCase(Locale.ROOT).replace('_', '-');
128 }
129
130 @Override
131 public boolean matchConfigValue(String in) {
132 if (StringUtils.isEmptyOrNull(in))
133 return false;
134 if (!in.startsWith("--"))
135 return false;
136 return name().equalsIgnoreCase(in.substring(2).replace('-', '_'));
137 }
138
139
140
141
142
143 public enum Merge {
144
145
146
147 TRUE,
148
149
150
151 FALSE,
152
153
154
155 ONLY;
156
157
158
159
160
161
162
163
164
165 public static Merge valueOf(FastForwardMode ffMode) {
166 switch (ffMode) {
167 case NO_FF:
168 return FALSE;
169 case FF_ONLY:
170 return ONLY;
171 default:
172 return TRUE;
173 }
174 }
175 }
176
177
178
179
180
181
182
183
184
185 public static FastForwardMode valueOf(FastForwardMode.Merge ffMode) {
186 switch (ffMode) {
187 case FALSE:
188 return NO_FF;
189 case ONLY:
190 return FF_ONLY;
191 default:
192 return FF;
193 }
194 }
195 }
196
197 private Boolean commit;
198
199
200
201
202
203
204
205 protected MergeCommand(Repository repo) {
206 super(repo);
207 }
208
209
210
211
212
213
214
215
216
217 @Override
218 @SuppressWarnings("boxing")
219 public MergeResult call() throws GitAPIException, NoHeadException,
220 ConcurrentRefUpdateException, CheckoutConflictException,
221 InvalidMergeHeadsException, WrongRepositoryStateException, NoMessageException {
222 checkCallable();
223 fallBackToConfiguration();
224 checkParameters();
225
226 DirCacheCheckout dco = null;
227 try (RevWalklk.html#RevWalk">RevWalk revWalk = new RevWalk(repo)) {
228 Ref head = repo.exactRef(Constants.HEAD);
229 if (head == null)
230 throw new NoHeadException(
231 JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
232 StringBuilder refLogMessage = new StringBuilder("merge ");
233
234
235
236
237 Ref ref = commits.get(0);
238
239 refLogMessage.append(ref.getName());
240
241
242 ref = repo.getRefDatabase().peel(ref);
243 ObjectId objectId = ref.getPeeledObjectId();
244 if (objectId == null)
245 objectId = ref.getObjectId();
246
247 RevCommit srcCommit = revWalk.lookupCommit(objectId);
248
249 ObjectId headId = head.getObjectId();
250 if (headId == null) {
251 revWalk.parseHeaders(srcCommit);
252 dco = new DirCacheCheckout(repo,
253 repo.lockDirCache(), srcCommit.getTree());
254 dco.setFailOnConflict(true);
255 dco.setProgressMonitor(monitor);
256 dco.checkout();
257 RefUpdate refUpdate = repo
258 .updateRef(head.getTarget().getName());
259 refUpdate.setNewObjectId(objectId);
260 refUpdate.setExpectedOldObjectId(null);
261 refUpdate.setRefLogMessage("initial pull", false);
262 if (refUpdate.update() != Result.NEW)
263 throw new NoHeadException(
264 JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
265 setCallable(false);
266 return new MergeResult(srcCommit, srcCommit, new ObjectId[] {
267 null, srcCommit }, MergeStatus.FAST_FORWARD,
268 mergeStrategy, null, null);
269 }
270
271 RevCommit headCommit = revWalk.lookupCommit(headId);
272
273 if (revWalk.isMergedInto(srcCommit, headCommit)) {
274 setCallable(false);
275 return new MergeResult(headCommit, srcCommit, new ObjectId[] {
276 headCommit, srcCommit },
277 MergeStatus.ALREADY_UP_TO_DATE, mergeStrategy, null, null);
278 } else if (revWalk.isMergedInto(headCommit, srcCommit)
279 && fastForwardMode != FastForwardMode.NO_FF) {
280
281
282 refLogMessage.append(": " + MergeStatus.FAST_FORWARD);
283 dco = new DirCacheCheckout(repo,
284 headCommit.getTree(), repo.lockDirCache(),
285 srcCommit.getTree());
286 dco.setProgressMonitor(monitor);
287 dco.setFailOnConflict(true);
288 dco.checkout();
289 String msg = null;
290 ObjectId newHead, base = null;
291 MergeStatus mergeStatus = null;
292 if (!squash) {
293 updateHead(refLogMessage, srcCommit, headId);
294 newHead = base = srcCommit;
295 mergeStatus = MergeStatus.FAST_FORWARD;
296 } else {
297 msg = JGitText.get().squashCommitNotUpdatingHEAD;
298 newHead = base = headId;
299 mergeStatus = MergeStatus.FAST_FORWARD_SQUASHED;
300 List<RevCommit> squashedCommits = RevWalkUtils.find(
301 revWalk, srcCommit, headCommit);
302 String squashMessage = new SquashMessageFormatter().format(
303 squashedCommits, head);
304 repo.writeSquashCommitMsg(squashMessage);
305 }
306 setCallable(false);
307 return new MergeResult(newHead, base, new ObjectId[] {
308 headCommit, srcCommit }, mergeStatus, mergeStrategy,
309 null, msg);
310 } else {
311 if (fastForwardMode == FastForwardMode.FF_ONLY) {
312 return new MergeResult(headCommit, srcCommit,
313 new ObjectId[] { headCommit, srcCommit },
314 MergeStatus.ABORTED, mergeStrategy, null, null);
315 }
316 String mergeMessage = "";
317 if (!squash) {
318 if (message != null)
319 mergeMessage = message;
320 else
321 mergeMessage = new MergeMessageFormatter().format(
322 commits, head);
323 repo.writeMergeCommitMsg(mergeMessage);
324 repo.writeMergeHeads(Arrays.asList(ref.getObjectId()));
325 } else {
326 List<RevCommit> squashedCommits = RevWalkUtils.find(
327 revWalk, srcCommit, headCommit);
328 String squashMessage = new SquashMessageFormatter().format(
329 squashedCommits, head);
330 repo.writeSquashCommitMsg(squashMessage);
331 }
332 Merger merger = mergeStrategy.newMerger(repo);
333 merger.setProgressMonitor(monitor);
334 boolean noProblems;
335 Map<String, org.eclipse.jgit.merge.MergeResult<?>> lowLevelResults = null;
336 Map<String, MergeFailureReason> failingPaths = null;
337 List<String> unmergedPaths = null;
338 if (merger instanceof ResolveMerger) {
339 ResolveMerger resolveMerger = (ResolveMerger) merger;
340 resolveMerger.setContentMergeStrategy(contentStrategy);
341 resolveMerger.setCommitNames(new String[] {
342 "BASE", "HEAD", ref.getName() });
343 resolveMerger.setWorkingTreeIterator(new FileTreeIterator(repo));
344 noProblems = merger.merge(headCommit, srcCommit);
345 lowLevelResults = resolveMerger
346 .getMergeResults();
347 failingPaths = resolveMerger.getFailingPaths();
348 unmergedPaths = resolveMerger.getUnmergedPaths();
349 if (!resolveMerger.getModifiedFiles().isEmpty()) {
350 repo.fireEvent(new WorkingTreeModifiedEvent(
351 resolveMerger.getModifiedFiles(), null));
352 }
353 } else
354 noProblems = merger.merge(headCommit, srcCommit);
355 refLogMessage.append(": Merge made by ");
356 if (!revWalk.isMergedInto(headCommit, srcCommit))
357 refLogMessage.append(mergeStrategy.getName());
358 else
359 refLogMessage.append("recursive");
360 refLogMessage.append('.');
361 if (noProblems) {
362 dco = new DirCacheCheckout(repo,
363 headCommit.getTree(), repo.lockDirCache(),
364 merger.getResultTreeId());
365 dco.setFailOnConflict(true);
366 dco.setProgressMonitor(monitor);
367 dco.checkout();
368
369 String msg = null;
370 ObjectId newHeadId = null;
371 MergeStatus mergeStatus = null;
372 if (!commit && squash) {
373 mergeStatus = MergeStatus.MERGED_SQUASHED_NOT_COMMITTED;
374 }
375 if (!commit && !squash) {
376 mergeStatus = MergeStatus.MERGED_NOT_COMMITTED;
377 }
378 if (commit && !squash) {
379 try (Gitit.html#Git">Git git = new Git(getRepository())) {
380 newHeadId = git.commit()
381 .setReflogComment(refLogMessage.toString())
382 .setInsertChangeId(insertChangeId)
383 .call().getId();
384 }
385 mergeStatus = MergeStatus.MERGED;
386 getRepository().autoGC(monitor);
387 }
388 if (commit && squash) {
389 msg = JGitText.get().squashCommitNotUpdatingHEAD;
390 newHeadId = headCommit.getId();
391 mergeStatus = MergeStatus.MERGED_SQUASHED;
392 }
393 return new MergeResult(newHeadId, null,
394 new ObjectId[] { headCommit.getId(),
395 srcCommit.getId() }, mergeStatus,
396 mergeStrategy, null, msg);
397 }
398 if (failingPaths != null) {
399 repo.writeMergeCommitMsg(null);
400 repo.writeMergeHeads(null);
401 return new MergeResult(null, merger.getBaseCommitId(),
402 new ObjectId[] { headCommit.getId(),
403 srcCommit.getId() },
404 MergeStatus.FAILED, mergeStrategy, lowLevelResults,
405 failingPaths, null);
406 }
407 String mergeMessageWithConflicts = new MergeMessageFormatter()
408 .formatWithConflicts(mergeMessage, unmergedPaths);
409 repo.writeMergeCommitMsg(mergeMessageWithConflicts);
410 return new MergeResult(null, merger.getBaseCommitId(),
411 new ObjectId[] { headCommit.getId(),
412 srcCommit.getId() },
413 MergeStatus.CONFLICTING, mergeStrategy, lowLevelResults,
414 null);
415 }
416 } catch (org.eclipse.jgit.errors.CheckoutConflictException e) {
417 List<String> conflicts = (dco == null) ? Collections
418 .<String> emptyList() : dco.getConflicts();
419 throw new CheckoutConflictException(conflicts, e);
420 } catch (IOException e) {
421 throw new JGitInternalException(
422 MessageFormat.format(
423 JGitText.get().exceptionCaughtDuringExecutionOfMergeCommand,
424 e), e);
425 }
426 }
427
428 private void checkParameters() throws InvalidMergeHeadsException {
429 if (squash.booleanValue() && fastForwardMode == FastForwardMode.NO_FF) {
430 throw new JGitInternalException(
431 JGitText.get().cannotCombineSquashWithNoff);
432 }
433
434 if (commits.size() != 1)
435 throw new InvalidMergeHeadsException(
436 commits.isEmpty() ? JGitText.get().noMergeHeadSpecified
437 : MessageFormat.format(
438 JGitText.get().mergeStrategyDoesNotSupportHeads,
439 mergeStrategy.getName(),
440 Integer.valueOf(commits.size())));
441 }
442
443
444
445
446
447 private void fallBackToConfiguration() {
448 MergeConfig config = MergeConfig.getConfigForCurrentBranch(repo);
449 if (squash == null)
450 squash = Boolean.valueOf(config.isSquash());
451 if (commit == null)
452 commit = Boolean.valueOf(config.isCommit());
453 if (fastForwardMode == null)
454 fastForwardMode = config.getFastForwardMode();
455 }
456
457 private void updateHead(StringBuilder refLogMessage, ObjectId newHeadId,
458 ObjectId oldHeadID) throws IOException,
459 ConcurrentRefUpdateException {
460 RefUpdate refUpdate = repo.updateRef(Constants.HEAD);
461 refUpdate.setNewObjectId(newHeadId);
462 refUpdate.setRefLogMessage(refLogMessage.toString(), false);
463 refUpdate.setExpectedOldObjectId(oldHeadID);
464 Result rc = refUpdate.update();
465 switch (rc) {
466 case NEW:
467 case FAST_FORWARD:
468 return;
469 case REJECTED:
470 case LOCK_FAILURE:
471 throw new ConcurrentRefUpdateException(
472 JGitText.get().couldNotLockHEAD, refUpdate.getRef(), rc);
473 default:
474 throw new JGitInternalException(MessageFormat.format(
475 JGitText.get().updatingRefFailed, Constants.HEAD,
476 newHeadId.toString(), rc));
477 }
478 }
479
480
481
482
483
484
485
486
487 public MergeCommand setStrategy(MergeStrategy mergeStrategy) {
488 checkCallable();
489 this.mergeStrategy = mergeStrategy;
490 return this;
491 }
492
493
494
495
496
497
498
499
500
501
502
503 public MergeCommand setContentMergeStrategy(ContentMergeStrategy strategy) {
504 checkCallable();
505 this.contentStrategy = strategy;
506 return this;
507 }
508
509
510
511
512
513
514
515
516 public MergeCommand include(Ref aCommit) {
517 checkCallable();
518 commits.add(aCommit);
519 return this;
520 }
521
522
523
524
525
526
527
528
529 public MergeCommand include(AnyObjectId aCommit) {
530 return include(aCommit.getName(), aCommit);
531 }
532
533
534
535
536
537
538
539
540
541
542 public MergeCommand include(String name, AnyObjectId aCommit) {
543 return include(new ObjectIdRef.Unpeeled(Storage.LOOSE, name,
544 aCommit.copy()));
545 }
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563 public MergeCommand setSquash(boolean squash) {
564 checkCallable();
565 this.squash = Boolean.valueOf(squash);
566 return this;
567 }
568
569
570
571
572
573
574
575
576
577
578
579
580 public MergeCommand setFastForward(
581 @Nullable FastForwardMode fastForwardMode) {
582 checkCallable();
583 this.fastForwardMode = fastForwardMode;
584 return this;
585 }
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603 public MergeCommand setCommit(boolean commit) {
604 this.commit = Boolean.valueOf(commit);
605 return this;
606 }
607
608
609
610
611
612
613
614
615
616
617 public MergeCommand setMessage(String message) {
618 this.message = message;
619 return this;
620 }
621
622
623
624
625
626
627
628
629
630
631
632
633 public MergeCommand setInsertChangeId(boolean insertChangeId) {
634 checkCallable();
635 this.insertChangeId = insertChangeId;
636 return this;
637 }
638
639
640
641
642
643
644
645
646
647
648
649 public MergeCommand setProgressMonitor(ProgressMonitor monitor) {
650 if (monitor == null) {
651 monitor = NullProgressMonitor.INSTANCE;
652 }
653 this.monitor = monitor;
654 return this;
655 }
656 }