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.EmtpyCommitException;
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 protected CommitCommand(Repository repo) {
146 super(repo);
147 }
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171 @Override
172 public RevCommit call() throws GitAPIException, NoHeadException,
173 NoMessageException, UnmergedPathsException,
174 ConcurrentRefUpdateException, WrongRepositoryStateException,
175 AbortedByHookException {
176 checkCallable();
177 Collections.sort(only);
178
179 try (RevWalk rw = new RevWalk(repo)) {
180 RepositoryState state = repo.getRepositoryState();
181 if (!state.canCommit())
182 throw new WrongRepositoryStateException(MessageFormat.format(
183 JGitText.get().cannotCommitOnARepoWithState,
184 state.name()));
185
186 if (!noVerify) {
187 Hooks.preCommit(repo, hookOutRedirect.get(PreCommitHook.NAME))
188 .call();
189 }
190
191 processOptions(state, rw);
192
193 if (all && !repo.isBare()) {
194 try (Git git = new Git(repo)) {
195 git.add()
196 .addFilepattern(".")
197 .setUpdate(true).call();
198 } catch (NoFilepatternException e) {
199
200 throw new JGitInternalException(e.getMessage(), e);
201 }
202 }
203
204 Ref head = repo.exactRef(Constants.HEAD);
205 if (head == null)
206 throw new NoHeadException(
207 JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
208
209
210 ObjectId headId = repo.resolve(Constants.HEAD + "^{commit}");
211 if (headId == null && amend)
212 throw new WrongRepositoryStateException(
213 JGitText.get().commitAmendOnInitialNotPossible);
214
215 if (headId != null)
216 if (amend) {
217 RevCommit previousCommit = rw.parseCommit(headId);
218 for (RevCommit p : previousCommit.getParents())
219 parents.add(p.getId());
220 if (author == null)
221 author = previousCommit.getAuthorIdent();
222 } else {
223 parents.add(0, headId);
224 }
225
226 if (!noVerify) {
227 message = Hooks
228 .commitMsg(repo,
229 hookOutRedirect.get(CommitMsgHook.NAME))
230 .setCommitMessage(message).call();
231 }
232
233
234 DirCache index = repo.lockDirCache();
235 try (ObjectInserter odi = repo.newObjectInserter()) {
236 if (!only.isEmpty())
237 index = createTemporaryIndex(headId, index, rw);
238
239
240
241
242 ObjectId indexTreeId = index.writeTree(odi);
243
244 if (insertChangeId)
245 insertChangeId(indexTreeId);
246
247
248 if (headId != null && !allowEmpty.booleanValue()) {
249 RevCommit headCommit = rw.parseCommit(headId);
250 headCommit.getTree();
251 if (indexTreeId.equals(headCommit.getTree())) {
252 throw new EmtpyCommitException(
253 JGitText.get().emptyCommit);
254 }
255 }
256
257
258 CommitBuilder commit = new CommitBuilder();
259 commit.setCommitter(committer);
260 commit.setAuthor(author);
261 commit.setMessage(message);
262
263 commit.setParentIds(parents);
264 commit.setTreeId(indexTreeId);
265 ObjectId commitId = odi.insert(commit);
266 odi.flush();
267
268 RevCommit revCommit = rw.parseCommit(commitId);
269 RefUpdate ru = repo.updateRef(Constants.HEAD);
270 ru.setNewObjectId(commitId);
271 if (!useDefaultReflogMessage) {
272 ru.setRefLogMessage(reflogComment, false);
273 } else {
274 String prefix = amend ? "commit (amend): "
275 : parents.size() == 0 ? "commit (initial): "
276 : "commit: ";
277 ru.setRefLogMessage(prefix + revCommit.getShortMessage(),
278 false);
279 }
280 if (headId != null)
281 ru.setExpectedOldObjectId(headId);
282 else
283 ru.setExpectedOldObjectId(ObjectId.zeroId());
284 Result rc = ru.forceUpdate();
285 switch (rc) {
286 case NEW:
287 case FORCED:
288 case FAST_FORWARD: {
289 setCallable(false);
290 if (state == RepositoryState.MERGING_RESOLVED
291 || isMergeDuringRebase(state)) {
292
293
294 repo.writeMergeCommitMsg(null);
295 repo.writeMergeHeads(null);
296 } else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) {
297 repo.writeMergeCommitMsg(null);
298 repo.writeCherryPickHead(null);
299 } else if (state == RepositoryState.REVERTING_RESOLVED) {
300 repo.writeMergeCommitMsg(null);
301 repo.writeRevertHead(null);
302 }
303 Hooks.postCommit(repo,
304 hookOutRedirect.get(PostCommitHook.NAME)).call();
305
306 return revCommit;
307 }
308 case REJECTED:
309 case LOCK_FAILURE:
310 throw new ConcurrentRefUpdateException(
311 JGitText.get().couldNotLockHEAD, ru.getRef(), rc);
312 default:
313 throw new JGitInternalException(MessageFormat.format(
314 JGitText.get().updatingRefFailed, Constants.HEAD,
315 commitId.toString(), rc));
316 }
317 } finally {
318 index.unlock();
319 }
320 } catch (UnmergedPathException e) {
321 throw new UnmergedPathsException(e);
322 } catch (IOException e) {
323 throw new JGitInternalException(
324 JGitText.get().exceptionCaughtDuringExecutionOfCommitCommand, e);
325 }
326 }
327
328 private void insertChangeId(ObjectId treeId) {
329 ObjectId firstParentId = null;
330 if (!parents.isEmpty())
331 firstParentId = parents.get(0);
332 ObjectId changeId = ChangeIdUtil.computeChangeId(treeId, firstParentId,
333 author, committer, message);
334 message = ChangeIdUtil.insertId(message, changeId);
335 if (changeId != null)
336 message = message.replaceAll("\nChange-Id: I"
337 + ObjectId.zeroId().getName() + "\n", "\nChange-Id: I"
338 + changeId.getName() + "\n");
339 }
340
341 private DirCache createTemporaryIndex(ObjectId headId, DirCache index,
342 RevWalk rw)
343 throws IOException {
344 ObjectInserter inserter = null;
345
346
347 DirCacheBuilder existingBuilder = index.builder();
348
349
350
351 DirCache inCoreIndex = DirCache.newInCore();
352 DirCacheBuilder tempBuilder = inCoreIndex.builder();
353
354 onlyProcessed = new boolean[only.size()];
355 boolean emptyCommit = true;
356
357 try (TreeWalk treeWalk = new TreeWalk(repo)) {
358 treeWalk.setOperationType(OperationType.CHECKIN_OP);
359 int dcIdx = treeWalk
360 .addTree(new DirCacheBuildIterator(existingBuilder));
361 FileTreeIterator fti = new FileTreeIterator(repo);
362 fti.setDirCacheIterator(treeWalk, 0);
363 int fIdx = treeWalk.addTree(fti);
364 int hIdx = -1;
365 if (headId != null)
366 hIdx = treeWalk.addTree(rw.parseTree(headId));
367 treeWalk.setRecursive(true);
368
369 String lastAddedFile = null;
370 while (treeWalk.next()) {
371 String path = treeWalk.getPathString();
372
373 int pos = lookupOnly(path);
374
375 CanonicalTreeParser hTree = null;
376 if (hIdx != -1)
377 hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
378
379 DirCacheIterator dcTree = treeWalk.getTree(dcIdx,
380 DirCacheIterator.class);
381
382 if (pos >= 0) {
383
384
385 FileTreeIterator fTree = treeWalk.getTree(fIdx,
386 FileTreeIterator.class);
387
388
389 boolean tracked = dcTree != null || hTree != null;
390 if (!tracked)
391 continue;
392
393
394
395 if (path.equals(lastAddedFile))
396 continue;
397
398 lastAddedFile = path;
399
400 if (fTree != null) {
401
402
403 final DirCacheEntry dcEntry = new DirCacheEntry(path);
404 long entryLength = fTree.getEntryLength();
405 dcEntry.setLength(entryLength);
406 dcEntry.setLastModified(fTree.getEntryLastModified());
407 dcEntry.setFileMode(fTree.getIndexFileMode(dcTree));
408
409 boolean objectExists = (dcTree != null
410 && fTree.idEqual(dcTree))
411 || (hTree != null && fTree.idEqual(hTree));
412 if (objectExists) {
413 dcEntry.setObjectId(fTree.getEntryObjectId());
414 } else {
415 if (FileMode.GITLINK.equals(dcEntry.getFileMode()))
416 dcEntry.setObjectId(fTree.getEntryObjectId());
417 else {
418
419 if (inserter == null)
420 inserter = repo.newObjectInserter();
421 long contentLength = fTree
422 .getEntryContentLength();
423 InputStream inputStream = fTree
424 .openEntryStream();
425 try {
426 dcEntry.setObjectId(inserter.insert(
427 Constants.OBJ_BLOB, contentLength,
428 inputStream));
429 } finally {
430 inputStream.close();
431 }
432 }
433 }
434
435
436 existingBuilder.add(dcEntry);
437
438 tempBuilder.add(dcEntry);
439
440 if (emptyCommit
441 && (hTree == null || !hTree.idEqual(fTree)
442 || hTree.getEntryRawMode() != fTree
443 .getEntryRawMode()))
444
445 emptyCommit = false;
446 } else {
447
448
449
450 if (emptyCommit && hTree != null)
451
452 emptyCommit = false;
453 }
454
455
456 onlyProcessed[pos] = true;
457 } else {
458
459 if (hTree != null) {
460
461
462 final DirCacheEntry dcEntry = new DirCacheEntry(path);
463 dcEntry.setObjectId(hTree.getEntryObjectId());
464 dcEntry.setFileMode(hTree.getEntryFileMode());
465
466
467 tempBuilder.add(dcEntry);
468 }
469
470
471 if (dcTree != null)
472 existingBuilder.add(dcTree.getDirCacheEntry());
473 }
474 }
475 }
476
477
478
479 for (int i = 0; i < onlyProcessed.length; i++)
480 if (!onlyProcessed[i])
481 throw new JGitInternalException(MessageFormat.format(
482 JGitText.get().entryNotFoundByPath, only.get(i)));
483
484
485 if (emptyCommit)
486
487
488 throw new JGitInternalException(JGitText.get().emptyCommit);
489
490
491 existingBuilder.commit();
492
493 tempBuilder.finish();
494 return inCoreIndex;
495 }
496
497
498
499
500
501
502
503
504
505
506
507
508
509 private int lookupOnly(String pathString) {
510 String p = pathString;
511 while (true) {
512 int position = Collections.binarySearch(only, p);
513 if (position >= 0)
514 return position;
515 int l = p.lastIndexOf("/");
516 if (l < 1)
517 break;
518 p = p.substring(0, l);
519 }
520 return -1;
521 }
522
523
524
525
526
527
528
529
530
531
532
533
534
535 private void processOptions(RepositoryState state, RevWalk rw)
536 throws NoMessageException {
537 if (committer == null)
538 committer = new PersonIdent(repo);
539 if (author == null && !amend)
540 author = committer;
541 if (allowEmpty == null)
542
543
544
545
546 allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE;
547
548
549 if (state == RepositoryState.MERGING_RESOLVED
550 || isMergeDuringRebase(state)) {
551 try {
552 parents = repo.readMergeHeads();
553 if (parents != null)
554 for (int i = 0; i < parents.size(); i++) {
555 RevObject ro = rw.parseAny(parents.get(i));
556 if (ro instanceof RevTag)
557 parents.set(i, rw.peel(ro));
558 }
559 } catch (IOException e) {
560 throw new JGitInternalException(MessageFormat.format(
561 JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
562 Constants.MERGE_HEAD, e), e);
563 }
564 if (message == null) {
565 try {
566 message = repo.readMergeCommitMsg();
567 } catch (IOException e) {
568 throw new JGitInternalException(MessageFormat.format(
569 JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
570 Constants.MERGE_MSG, e), e);
571 }
572 }
573 } else if (state == RepositoryState.SAFE && message == null) {
574 try {
575 message = repo.readSquashCommitMsg();
576 if (message != null)
577 repo.writeSquashCommitMsg(null );
578 } catch (IOException e) {
579 throw new JGitInternalException(MessageFormat.format(
580 JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
581 Constants.MERGE_MSG, e), e);
582 }
583
584 }
585 if (message == null)
586
587
588 throw new NoMessageException(JGitText.get().commitMessageNotSpecified);
589 }
590
591 private boolean isMergeDuringRebase(RepositoryState state) {
592 if (state != RepositoryState.REBASING_INTERACTIVE
593 && state != RepositoryState.REBASING_MERGE)
594 return false;
595 try {
596 return repo.readMergeHeads() != null;
597 } catch (IOException e) {
598 throw new JGitInternalException(MessageFormat.format(
599 JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
600 Constants.MERGE_HEAD, e), e);
601 }
602 }
603
604
605
606
607
608
609 public CommitCommand setMessage(String message) {
610 checkCallable();
611 this.message = message;
612 return this;
613 }
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631 public CommitCommand setAllowEmpty(boolean allowEmpty) {
632 this.allowEmpty = Boolean.valueOf(allowEmpty);
633 return this;
634 }
635
636
637
638
639 public String getMessage() {
640 return message;
641 }
642
643
644
645
646
647
648
649
650
651
652
653 public CommitCommand setCommitter(PersonIdent committer) {
654 checkCallable();
655 this.committer = committer;
656 return this;
657 }
658
659
660
661
662
663
664
665
666
667
668
669
670 public CommitCommand setCommitter(String name, String email) {
671 checkCallable();
672 return setCommitter(new PersonIdent(name, email));
673 }
674
675
676
677
678
679
680
681 public PersonIdent getCommitter() {
682 return committer;
683 }
684
685
686
687
688
689
690
691
692
693
694
695 public CommitCommand setAuthor(PersonIdent author) {
696 checkCallable();
697 this.author = author;
698 return this;
699 }
700
701
702
703
704
705
706
707
708
709
710
711
712 public CommitCommand setAuthor(String name, String email) {
713 checkCallable();
714 return setAuthor(new PersonIdent(name, email));
715 }
716
717
718
719
720
721
722
723 public PersonIdent getAuthor() {
724 return author;
725 }
726
727
728
729
730
731
732
733
734
735
736
737 public CommitCommand setAll(boolean all) {
738 checkCallable();
739 if (all && !only.isEmpty())
740 throw new JGitInternalException(MessageFormat.format(
741 JGitText.get().illegalCombinationOfArguments, "--all",
742 "--only"));
743 this.all = all;
744 return this;
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 }