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