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