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 package org.eclipse.jgit.api;
44
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.PrintStream;
48 import java.text.MessageFormat;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.HashMap;
52 import java.util.LinkedList;
53 import java.util.List;
54
55 import org.eclipse.jgit.api.errors.AbortedByHookException;
56 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
57 import org.eclipse.jgit.api.errors.EmptyCommitException;
58 import org.eclipse.jgit.api.errors.GitAPIException;
59 import org.eclipse.jgit.api.errors.JGitInternalException;
60 import org.eclipse.jgit.api.errors.NoFilepatternException;
61 import org.eclipse.jgit.api.errors.NoHeadException;
62 import org.eclipse.jgit.api.errors.NoMessageException;
63 import org.eclipse.jgit.api.errors.UnmergedPathsException;
64 import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
65 import org.eclipse.jgit.dircache.DirCache;
66 import org.eclipse.jgit.dircache.DirCacheBuildIterator;
67 import org.eclipse.jgit.dircache.DirCacheBuilder;
68 import org.eclipse.jgit.dircache.DirCacheEntry;
69 import org.eclipse.jgit.dircache.DirCacheIterator;
70 import org.eclipse.jgit.errors.UnmergedPathException;
71 import org.eclipse.jgit.hooks.CommitMsgHook;
72 import org.eclipse.jgit.hooks.Hooks;
73 import org.eclipse.jgit.hooks.PostCommitHook;
74 import org.eclipse.jgit.hooks.PreCommitHook;
75 import org.eclipse.jgit.internal.JGitText;
76 import org.eclipse.jgit.lib.CommitBuilder;
77 import org.eclipse.jgit.lib.Constants;
78 import org.eclipse.jgit.lib.FileMode;
79 import org.eclipse.jgit.lib.ObjectId;
80 import org.eclipse.jgit.lib.ObjectInserter;
81 import org.eclipse.jgit.lib.PersonIdent;
82 import org.eclipse.jgit.lib.Ref;
83 import org.eclipse.jgit.lib.RefUpdate;
84 import org.eclipse.jgit.lib.RefUpdate.Result;
85 import org.eclipse.jgit.lib.Repository;
86 import org.eclipse.jgit.lib.RepositoryState;
87 import org.eclipse.jgit.revwalk.RevCommit;
88 import org.eclipse.jgit.revwalk.RevObject;
89 import org.eclipse.jgit.revwalk.RevTag;
90 import org.eclipse.jgit.revwalk.RevWalk;
91 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
92 import org.eclipse.jgit.treewalk.FileTreeIterator;
93 import org.eclipse.jgit.treewalk.TreeWalk;
94 import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
95 import org.eclipse.jgit.util.ChangeIdUtil;
96
97
98
99
100
101
102
103
104
105
106 public class CommitCommand extends GitCommand<RevCommit> {
107 private PersonIdent author;
108
109 private PersonIdent committer;
110
111 private String message;
112
113 private boolean all;
114
115 private List<String> only = new ArrayList<>();
116
117 private boolean[] onlyProcessed;
118
119 private boolean amend;
120
121 private boolean insertChangeId;
122
123
124
125
126
127 private List<ObjectId> parents = new LinkedList<>();
128
129 private String reflogComment;
130
131 private boolean useDefaultReflogMessage = true;
132
133
134
135
136 private boolean noVerify;
137
138 private HashMap<String, PrintStream> hookOutRedirect = new HashMap<>(3);
139
140 private Boolean allowEmpty;
141
142
143
144
145
146
147
148 protected CommitCommand(Repository repo) {
149 super(repo);
150 }
151
152
153
154
155
156
157
158
159
160 @Override
161 public RevCommit call() throws GitAPIException, NoHeadException,
162 NoMessageException, UnmergedPathsException,
163 ConcurrentRefUpdateException, WrongRepositoryStateException,
164 AbortedByHookException {
165 checkCallable();
166 Collections.sort(only);
167
168 try (RevWalk rw = new RevWalk(repo)) {
169 RepositoryState state = repo.getRepositoryState();
170 if (!state.canCommit())
171 throw new WrongRepositoryStateException(MessageFormat.format(
172 JGitText.get().cannotCommitOnARepoWithState,
173 state.name()));
174
175 if (!noVerify) {
176 Hooks.preCommit(repo, hookOutRedirect.get(PreCommitHook.NAME))
177 .call();
178 }
179
180 processOptions(state, rw);
181
182 if (all && !repo.isBare()) {
183 try (Git git = new Git(repo)) {
184 git.add()
185 .addFilepattern(".")
186 .setUpdate(true).call();
187 } catch (NoFilepatternException e) {
188
189 throw new JGitInternalException(e.getMessage(), e);
190 }
191 }
192
193 Ref head = repo.exactRef(Constants.HEAD);
194 if (head == null)
195 throw new NoHeadException(
196 JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
197
198
199 ObjectId headId = repo.resolve(Constants.HEAD + "^{commit}");
200 if (headId == null && amend)
201 throw new WrongRepositoryStateException(
202 JGitText.get().commitAmendOnInitialNotPossible);
203
204 if (headId != null)
205 if (amend) {
206 RevCommit previousCommit = rw.parseCommit(headId);
207 for (RevCommit p : previousCommit.getParents())
208 parents.add(p.getId());
209 if (author == null)
210 author = previousCommit.getAuthorIdent();
211 } else {
212 parents.add(0, headId);
213 }
214
215 if (!noVerify) {
216 message = Hooks
217 .commitMsg(repo,
218 hookOutRedirect.get(CommitMsgHook.NAME))
219 .setCommitMessage(message).call();
220 }
221
222
223 DirCache index = repo.lockDirCache();
224 try (ObjectInserter odi = repo.newObjectInserter()) {
225 if (!only.isEmpty())
226 index = createTemporaryIndex(headId, index, rw);
227
228
229
230
231 ObjectId indexTreeId = index.writeTree(odi);
232
233 if (insertChangeId)
234 insertChangeId(indexTreeId);
235
236
237 if (headId != null && !allowEmpty.booleanValue()) {
238 RevCommit headCommit = rw.parseCommit(headId);
239 headCommit.getTree();
240 if (indexTreeId.equals(headCommit.getTree())) {
241 throw new EmptyCommitException(
242 JGitText.get().emptyCommit);
243 }
244 }
245
246
247 CommitBuilder commit = new CommitBuilder();
248 commit.setCommitter(committer);
249 commit.setAuthor(author);
250 commit.setMessage(message);
251
252 commit.setParentIds(parents);
253 commit.setTreeId(indexTreeId);
254 ObjectId commitId = odi.insert(commit);
255 odi.flush();
256
257 RevCommit revCommit = rw.parseCommit(commitId);
258 RefUpdate ru = repo.updateRef(Constants.HEAD);
259 ru.setNewObjectId(commitId);
260 if (!useDefaultReflogMessage) {
261 ru.setRefLogMessage(reflogComment, false);
262 } else {
263 String prefix = amend ? "commit (amend): "
264 : parents.size() == 0 ? "commit (initial): "
265 : "commit: ";
266 ru.setRefLogMessage(prefix + revCommit.getShortMessage(),
267 false);
268 }
269 if (headId != null)
270 ru.setExpectedOldObjectId(headId);
271 else
272 ru.setExpectedOldObjectId(ObjectId.zeroId());
273 Result rc = ru.forceUpdate();
274 switch (rc) {
275 case NEW:
276 case FORCED:
277 case FAST_FORWARD: {
278 setCallable(false);
279 if (state == RepositoryState.MERGING_RESOLVED
280 || isMergeDuringRebase(state)) {
281
282
283 repo.writeMergeCommitMsg(null);
284 repo.writeMergeHeads(null);
285 } else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) {
286 repo.writeMergeCommitMsg(null);
287 repo.writeCherryPickHead(null);
288 } else if (state == RepositoryState.REVERTING_RESOLVED) {
289 repo.writeMergeCommitMsg(null);
290 repo.writeRevertHead(null);
291 }
292 Hooks.postCommit(repo,
293 hookOutRedirect.get(PostCommitHook.NAME)).call();
294
295 return revCommit;
296 }
297 case REJECTED:
298 case LOCK_FAILURE:
299 throw new ConcurrentRefUpdateException(
300 JGitText.get().couldNotLockHEAD, ru.getRef(), rc);
301 default:
302 throw new JGitInternalException(MessageFormat.format(
303 JGitText.get().updatingRefFailed, Constants.HEAD,
304 commitId.toString(), rc));
305 }
306 } finally {
307 index.unlock();
308 }
309 } catch (UnmergedPathException e) {
310 throw new UnmergedPathsException(e);
311 } catch (IOException e) {
312 throw new JGitInternalException(
313 JGitText.get().exceptionCaughtDuringExecutionOfCommitCommand, e);
314 }
315 }
316
317 private void insertChangeId(ObjectId treeId) {
318 ObjectId firstParentId = null;
319 if (!parents.isEmpty())
320 firstParentId = parents.get(0);
321 ObjectId changeId = ChangeIdUtil.computeChangeId(treeId, firstParentId,
322 author, committer, message);
323 message = ChangeIdUtil.insertId(message, changeId);
324 if (changeId != null)
325 message = message.replaceAll("\nChange-Id: I"
326 + ObjectId.zeroId().getName() + "\n", "\nChange-Id: I"
327 + changeId.getName() + "\n");
328 }
329
330 private DirCache createTemporaryIndex(ObjectId headId, DirCache index,
331 RevWalk rw)
332 throws IOException {
333 ObjectInserter inserter = null;
334
335
336 DirCacheBuilder existingBuilder = index.builder();
337
338
339
340 DirCache inCoreIndex = DirCache.newInCore();
341 DirCacheBuilder tempBuilder = inCoreIndex.builder();
342
343 onlyProcessed = new boolean[only.size()];
344 boolean emptyCommit = true;
345
346 try (TreeWalk treeWalk = new TreeWalk(repo)) {
347 treeWalk.setOperationType(OperationType.CHECKIN_OP);
348 int dcIdx = treeWalk
349 .addTree(new DirCacheBuildIterator(existingBuilder));
350 FileTreeIterator fti = new FileTreeIterator(repo);
351 fti.setDirCacheIterator(treeWalk, 0);
352 int fIdx = treeWalk.addTree(fti);
353 int hIdx = -1;
354 if (headId != null)
355 hIdx = treeWalk.addTree(rw.parseTree(headId));
356 treeWalk.setRecursive(true);
357
358 String lastAddedFile = null;
359 while (treeWalk.next()) {
360 String path = treeWalk.getPathString();
361
362 int pos = lookupOnly(path);
363
364 CanonicalTreeParser hTree = null;
365 if (hIdx != -1)
366 hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
367
368 DirCacheIterator dcTree = treeWalk.getTree(dcIdx,
369 DirCacheIterator.class);
370
371 if (pos >= 0) {
372
373
374 FileTreeIterator fTree = treeWalk.getTree(fIdx,
375 FileTreeIterator.class);
376
377
378 boolean tracked = dcTree != null || hTree != null;
379 if (!tracked)
380 continue;
381
382
383
384 if (path.equals(lastAddedFile))
385 continue;
386
387 lastAddedFile = path;
388
389 if (fTree != null) {
390
391
392 final DirCacheEntry dcEntry = new DirCacheEntry(path);
393 long entryLength = fTree.getEntryLength();
394 dcEntry.setLength(entryLength);
395 dcEntry.setLastModified(fTree.getEntryLastModifiedInstant());
396 dcEntry.setFileMode(fTree.getIndexFileMode(dcTree));
397
398 boolean objectExists = (dcTree != null
399 && fTree.idEqual(dcTree))
400 || (hTree != null && fTree.idEqual(hTree));
401 if (objectExists) {
402 dcEntry.setObjectId(fTree.getEntryObjectId());
403 } else {
404 if (FileMode.GITLINK.equals(dcEntry.getFileMode()))
405 dcEntry.setObjectId(fTree.getEntryObjectId());
406 else {
407
408 if (inserter == null)
409 inserter = repo.newObjectInserter();
410 long contentLength = fTree
411 .getEntryContentLength();
412 try (InputStream inputStream = fTree
413 .openEntryStream()) {
414 dcEntry.setObjectId(inserter.insert(
415 Constants.OBJ_BLOB, contentLength,
416 inputStream));
417 }
418 }
419 }
420
421
422 existingBuilder.add(dcEntry);
423
424 tempBuilder.add(dcEntry);
425
426 if (emptyCommit
427 && (hTree == null || !hTree.idEqual(fTree)
428 || hTree.getEntryRawMode() != fTree
429 .getEntryRawMode()))
430
431 emptyCommit = false;
432 } else {
433
434
435
436 if (emptyCommit && hTree != null)
437
438 emptyCommit = false;
439 }
440
441
442 onlyProcessed[pos] = true;
443 } else {
444
445 if (hTree != null) {
446
447
448 final DirCacheEntry dcEntry = new DirCacheEntry(path);
449 dcEntry.setObjectId(hTree.getEntryObjectId());
450 dcEntry.setFileMode(hTree.getEntryFileMode());
451
452
453 tempBuilder.add(dcEntry);
454 }
455
456
457 if (dcTree != null)
458 existingBuilder.add(dcTree.getDirCacheEntry());
459 }
460 }
461 }
462
463
464
465 for (int i = 0; i < onlyProcessed.length; i++)
466 if (!onlyProcessed[i])
467 throw new JGitInternalException(MessageFormat.format(
468 JGitText.get().entryNotFoundByPath, only.get(i)));
469
470
471 if (emptyCommit && !allowEmpty.booleanValue())
472
473
474 throw new JGitInternalException(JGitText.get().emptyCommit);
475
476
477 existingBuilder.commit();
478
479 tempBuilder.finish();
480 return inCoreIndex;
481 }
482
483
484
485
486
487
488
489
490
491
492
493
494
495 private int lookupOnly(String pathString) {
496 String p = pathString;
497 while (true) {
498 int position = Collections.binarySearch(only, p);
499 if (position >= 0)
500 return position;
501 int l = p.lastIndexOf("/");
502 if (l < 1)
503 break;
504 p = p.substring(0, l);
505 }
506 return -1;
507 }
508
509
510
511
512
513
514
515
516
517
518
519
520
521 private void processOptions(RepositoryState state, RevWalk rw)
522 throws NoMessageException {
523 if (committer == null)
524 committer = new PersonIdent(repo);
525 if (author == null && !amend)
526 author = committer;
527 if (allowEmpty == null)
528
529
530
531
532 allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE;
533
534
535 if (state == RepositoryState.MERGING_RESOLVED
536 || isMergeDuringRebase(state)) {
537 try {
538 parents = repo.readMergeHeads();
539 if (parents != null)
540 for (int i = 0; i < parents.size(); i++) {
541 RevObject ro = rw.parseAny(parents.get(i));
542 if (ro instanceof RevTag)
543 parents.set(i, rw.peel(ro));
544 }
545 } catch (IOException e) {
546 throw new JGitInternalException(MessageFormat.format(
547 JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
548 Constants.MERGE_HEAD, e), e);
549 }
550 if (message == null) {
551 try {
552 message = repo.readMergeCommitMsg();
553 } catch (IOException e) {
554 throw new JGitInternalException(MessageFormat.format(
555 JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
556 Constants.MERGE_MSG, e), e);
557 }
558 }
559 } else if (state == RepositoryState.SAFE && message == null) {
560 try {
561 message = repo.readSquashCommitMsg();
562 if (message != null)
563 repo.writeSquashCommitMsg(null );
564 } catch (IOException e) {
565 throw new JGitInternalException(MessageFormat.format(
566 JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
567 Constants.MERGE_MSG, e), e);
568 }
569
570 }
571 if (message == null)
572
573
574 throw new NoMessageException(JGitText.get().commitMessageNotSpecified);
575 }
576
577 private boolean isMergeDuringRebase(RepositoryState state) {
578 if (state != RepositoryState.REBASING_INTERACTIVE
579 && state != RepositoryState.REBASING_MERGE)
580 return false;
581 try {
582 return repo.readMergeHeads() != null;
583 } catch (IOException e) {
584 throw new JGitInternalException(MessageFormat.format(
585 JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
586 Constants.MERGE_HEAD, e), e);
587 }
588 }
589
590
591
592
593
594
595
596
597 public CommitCommand setMessage(String message) {
598 checkCallable();
599 this.message = message;
600 return this;
601 }
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622 public CommitCommand setAllowEmpty(boolean allowEmpty) {
623 this.allowEmpty = Boolean.valueOf(allowEmpty);
624 return this;
625 }
626
627
628
629
630
631
632 public String getMessage() {
633 return message;
634 }
635
636
637
638
639
640
641
642
643
644
645
646 public CommitCommand setCommitter(PersonIdent committer) {
647 checkCallable();
648 this.committer = committer;
649 return this;
650 }
651
652
653
654
655
656
657
658
659
660
661
662
663 public CommitCommand setCommitter(String name, String email) {
664 checkCallable();
665 return setCommitter(new PersonIdent(name, email));
666 }
667
668
669
670
671
672
673
674
675
676 public PersonIdent getCommitter() {
677 return committer;
678 }
679
680
681
682
683
684
685
686
687
688
689
690 public CommitCommand setAuthor(PersonIdent author) {
691 checkCallable();
692 this.author = author;
693 return this;
694 }
695
696
697
698
699
700
701
702
703
704
705
706
707 public CommitCommand setAuthor(String name, String email) {
708 checkCallable();
709 return setAuthor(new PersonIdent(name, email));
710 }
711
712
713
714
715
716
717
718
719
720 public PersonIdent getAuthor() {
721 return author;
722 }
723
724
725
726
727
728
729
730
731
732
733
734
735
736 public CommitCommand setAll(boolean all) {
737 checkCallable();
738 if (all && !only.isEmpty())
739 throw new JGitInternalException(MessageFormat.format(
740 JGitText.get().illegalCombinationOfArguments, "--all",
741 "--only"));
742 this.all = all;
743 return this;
744 }
745
746
747
748
749
750
751
752
753
754
755 public CommitCommand setAmend(boolean amend) {
756 checkCallable();
757 this.amend = amend;
758 return this;
759 }
760
761
762
763
764
765
766
767
768
769
770
771
772 public CommitCommand setOnly(String only) {
773 checkCallable();
774 if (all)
775 throw new JGitInternalException(MessageFormat.format(
776 JGitText.get().illegalCombinationOfArguments, "--only",
777 "--all"));
778 String o = only.endsWith("/") ? only.substring(0, only.length() - 1)
779 : only;
780
781 if (!this.only.contains(o))
782 this.only.add(o);
783 return this;
784 }
785
786
787
788
789
790
791
792
793
794
795
796 public CommitCommand setInsertChangeId(boolean insertChangeId) {
797 checkCallable();
798 this.insertChangeId = insertChangeId;
799 return this;
800 }
801
802
803
804
805
806
807
808
809
810 public CommitCommand setReflogComment(String reflogComment) {
811 this.reflogComment = reflogComment;
812 useDefaultReflogMessage = false;
813 return this;
814 }
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830 public CommitCommand setNoVerify(boolean noVerify) {
831 this.noVerify = noVerify;
832 return this;
833 }
834
835
836
837
838
839
840
841
842
843
844
845 public CommitCommand setHookOutputStream(PrintStream hookStdOut) {
846 setHookOutputStream(PreCommitHook.NAME, hookStdOut);
847 setHookOutputStream(CommitMsgHook.NAME, hookStdOut);
848 setHookOutputStream(PostCommitHook.NAME, hookStdOut);
849 return this;
850 }
851
852
853
854
855
856
857
858
859
860
861
862
863
864 public CommitCommand setHookOutputStream(String hookName,
865 PrintStream hookStdOut) {
866 if (!(PreCommitHook.NAME.equals(hookName)
867 || CommitMsgHook.NAME.equals(hookName)
868 || PostCommitHook.NAME.equals(hookName))) {
869 throw new IllegalArgumentException(
870 MessageFormat.format(JGitText.get().illegalHookName,
871 hookName));
872 }
873 hookOutRedirect.put(hookName, hookStdOut);
874 return this;
875 }
876 }