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