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