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