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