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