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