CommitCommand.java

  1. /*
  2.  * Copyright (C) 2010-2012, Christian Halstrick <christian.halstrick@sap.com> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.api;

  11. import java.io.IOException;
  12. import java.io.InputStream;
  13. import java.io.PrintStream;
  14. import java.text.MessageFormat;
  15. import java.util.ArrayList;
  16. import java.util.Collections;
  17. import java.util.HashMap;
  18. import java.util.LinkedList;
  19. import java.util.List;

  20. import org.eclipse.jgit.annotations.NonNull;
  21. import org.eclipse.jgit.api.errors.AbortedByHookException;
  22. import org.eclipse.jgit.api.errors.CanceledException;
  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.ServiceUnavailableException;
  31. import org.eclipse.jgit.api.errors.UnmergedPathsException;
  32. import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
  33. import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
  34. import org.eclipse.jgit.dircache.DirCache;
  35. import org.eclipse.jgit.dircache.DirCacheBuildIterator;
  36. import org.eclipse.jgit.dircache.DirCacheBuilder;
  37. import org.eclipse.jgit.dircache.DirCacheEntry;
  38. import org.eclipse.jgit.dircache.DirCacheIterator;
  39. import org.eclipse.jgit.errors.IncorrectObjectTypeException;
  40. import org.eclipse.jgit.errors.MissingObjectException;
  41. import org.eclipse.jgit.errors.UnmergedPathException;
  42. import org.eclipse.jgit.hooks.CommitMsgHook;
  43. import org.eclipse.jgit.hooks.Hooks;
  44. import org.eclipse.jgit.hooks.PostCommitHook;
  45. import org.eclipse.jgit.hooks.PreCommitHook;
  46. import org.eclipse.jgit.internal.JGitText;
  47. import org.eclipse.jgit.lib.CommitBuilder;
  48. import org.eclipse.jgit.lib.CommitConfig;
  49. import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
  50. import org.eclipse.jgit.lib.Constants;
  51. import org.eclipse.jgit.lib.FileMode;
  52. import org.eclipse.jgit.lib.GpgConfig;
  53. import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
  54. import org.eclipse.jgit.lib.GpgObjectSigner;
  55. import org.eclipse.jgit.lib.GpgSigner;
  56. import org.eclipse.jgit.lib.ObjectId;
  57. import org.eclipse.jgit.lib.ObjectInserter;
  58. import org.eclipse.jgit.lib.PersonIdent;
  59. import org.eclipse.jgit.lib.Ref;
  60. import org.eclipse.jgit.lib.RefUpdate;
  61. import org.eclipse.jgit.lib.RefUpdate.Result;
  62. import org.eclipse.jgit.lib.Repository;
  63. import org.eclipse.jgit.lib.RepositoryState;
  64. import org.eclipse.jgit.revwalk.RevCommit;
  65. import org.eclipse.jgit.revwalk.RevObject;
  66. import org.eclipse.jgit.revwalk.RevTag;
  67. import org.eclipse.jgit.revwalk.RevWalk;
  68. import org.eclipse.jgit.transport.CredentialsProvider;
  69. import org.eclipse.jgit.treewalk.CanonicalTreeParser;
  70. import org.eclipse.jgit.treewalk.FileTreeIterator;
  71. import org.eclipse.jgit.treewalk.TreeWalk;
  72. import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
  73. import org.eclipse.jgit.util.ChangeIdUtil;
  74. import org.slf4j.Logger;
  75. import org.slf4j.LoggerFactory;

  76. /**
  77.  * A class used to execute a {@code Commit} command. It has setters for all
  78.  * supported options and arguments of this command and a {@link #call()} method
  79.  * to finally execute the command.
  80.  *
  81.  * @see <a
  82.  *      href="http://www.kernel.org/pub/software/scm/git/docs/git-commit.html"
  83.  *      >Git documentation about Commit</a>
  84.  */
  85. public class CommitCommand extends GitCommand<RevCommit> {
  86.     private static final Logger log = LoggerFactory
  87.             .getLogger(CommitCommand.class);

  88.     private PersonIdent author;

  89.     private PersonIdent committer;

  90.     private String message;

  91.     private boolean all;

  92.     private List<String> only = new ArrayList<>();

  93.     private boolean[] onlyProcessed;

  94.     private boolean amend;

  95.     private boolean insertChangeId;

  96.     /**
  97.      * parents this commit should have. The current HEAD will be in this list
  98.      * and also all commits mentioned in .git/MERGE_HEAD
  99.      */
  100.     private List<ObjectId> parents = new LinkedList<>();

  101.     private String reflogComment;

  102.     private boolean useDefaultReflogMessage = true;

  103.     /**
  104.      * Setting this option bypasses the pre-commit and commit-msg hooks.
  105.      */
  106.     private boolean noVerify;

  107.     private HashMap<String, PrintStream> hookOutRedirect = new HashMap<>(3);

  108.     private HashMap<String, PrintStream> hookErrRedirect = new HashMap<>(3);

  109.     private Boolean allowEmpty;

  110.     private Boolean signCommit;

  111.     private String signingKey;

  112.     private GpgSigner gpgSigner;

  113.     private GpgConfig gpgConfig;

  114.     private CredentialsProvider credentialsProvider;

  115.     private @NonNull CleanupMode cleanupMode = CleanupMode.VERBATIM;

  116.     private boolean cleanDefaultIsStrip = true;

  117.     private Character commentChar;

  118.     /**
  119.      * Constructor for CommitCommand
  120.      *
  121.      * @param repo
  122.      *            the {@link org.eclipse.jgit.lib.Repository}
  123.      */
  124.     protected CommitCommand(Repository repo) {
  125.         super(repo);
  126.         this.credentialsProvider = CredentialsProvider.getDefault();
  127.     }

  128.     /**
  129.      * {@inheritDoc}
  130.      * <p>
  131.      * Executes the {@code commit} command with all the options and parameters
  132.      * collected by the setter methods of this class. Each instance of this
  133.      * class should only be used for one invocation of the command (means: one
  134.      * call to {@link #call()})
  135.      *
  136.      * @throws ServiceUnavailableException
  137.      *             if signing service is not available e.g. since it isn't
  138.      *             installed
  139.      */
  140.     @Override
  141.     public RevCommit call() throws GitAPIException, AbortedByHookException,
  142.             ConcurrentRefUpdateException, NoHeadException, NoMessageException,
  143.             ServiceUnavailableException, UnmergedPathsException,
  144.             WrongRepositoryStateException {
  145.         checkCallable();
  146.         Collections.sort(only);

  147.         try (RevWalk rw = new RevWalk(repo)) {
  148.             RepositoryState state = repo.getRepositoryState();
  149.             if (!state.canCommit())
  150.                 throw new WrongRepositoryStateException(MessageFormat.format(
  151.                         JGitText.get().cannotCommitOnARepoWithState,
  152.                         state.name()));

  153.             if (!noVerify) {
  154.                 Hooks.preCommit(repo, hookOutRedirect.get(PreCommitHook.NAME),
  155.                         hookErrRedirect.get(PreCommitHook.NAME))
  156.                         .call();
  157.             }

  158.             processOptions(state, rw);

  159.             if (all && !repo.isBare()) {
  160.                 try (Git git = new Git(repo)) {
  161.                     git.add().addFilepattern(".") //$NON-NLS-1$
  162.                             .setUpdate(true).call();
  163.                 } catch (NoFilepatternException e) {
  164.                     // should really not happen
  165.                     throw new JGitInternalException(e.getMessage(), e);
  166.                 }
  167.             }

  168.             Ref head = repo.exactRef(Constants.HEAD);
  169.             if (head == null)
  170.                 throw new NoHeadException(
  171.                         JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);

  172.             // determine the current HEAD and the commit it is referring to
  173.             ObjectId headId = repo.resolve(Constants.HEAD + "^{commit}"); //$NON-NLS-1$
  174.             if (headId == null && amend)
  175.                 throw new WrongRepositoryStateException(
  176.                         JGitText.get().commitAmendOnInitialNotPossible);

  177.             if (headId != null) {
  178.                 if (amend) {
  179.                     RevCommit previousCommit = rw.parseCommit(headId);
  180.                     for (RevCommit p : previousCommit.getParents())
  181.                         parents.add(p.getId());
  182.                     if (author == null)
  183.                         author = previousCommit.getAuthorIdent();
  184.                 } else {
  185.                     parents.add(0, headId);
  186.                 }
  187.             }
  188.             if (!noVerify) {
  189.                 message = Hooks
  190.                         .commitMsg(repo,
  191.                                 hookOutRedirect.get(CommitMsgHook.NAME),
  192.                                 hookErrRedirect.get(CommitMsgHook.NAME))
  193.                         .setCommitMessage(message).call();
  194.             }

  195.             CommitConfig config = null;
  196.             if (CleanupMode.DEFAULT.equals(cleanupMode)) {
  197.                 config = repo.getConfig().get(CommitConfig.KEY);
  198.                 cleanupMode = config.resolve(cleanupMode, cleanDefaultIsStrip);
  199.             }
  200.             char comments = (char) 0;
  201.             if (CleanupMode.STRIP.equals(cleanupMode)
  202.                     || CleanupMode.SCISSORS.equals(cleanupMode)) {
  203.                 if (commentChar == null) {
  204.                     if (config == null) {
  205.                         config = repo.getConfig().get(CommitConfig.KEY);
  206.                     }
  207.                     if (config.isAutoCommentChar()) {
  208.                         // We're supposed to pick a character that isn't used,
  209.                         // but then cleaning up won't remove any lines. So don't
  210.                         // bother.
  211.                         comments = (char) 0;
  212.                         cleanupMode = CleanupMode.WHITESPACE;
  213.                     } else {
  214.                         comments = config.getCommentChar();
  215.                     }
  216.                 } else {
  217.                     comments = commentChar.charValue();
  218.                 }
  219.             }
  220.             message = CommitConfig.cleanText(message, cleanupMode, comments);

  221.             RevCommit revCommit;
  222.             DirCache index = repo.lockDirCache();
  223.             try (ObjectInserter odi = repo.newObjectInserter()) {
  224.                 if (!only.isEmpty())
  225.                     index = createTemporaryIndex(headId, index, rw);

  226.                 // Write the index as tree to the object database. This may
  227.                 // fail for example when the index contains unmerged paths
  228.                 // (unresolved conflicts)
  229.                 ObjectId indexTreeId = index.writeTree(odi);

  230.                 if (insertChangeId)
  231.                     insertChangeId(indexTreeId);

  232.                 checkIfEmpty(rw, headId, indexTreeId);

  233.                 // Create a Commit object, populate it and write it
  234.                 CommitBuilder commit = new CommitBuilder();
  235.                 commit.setCommitter(committer);
  236.                 commit.setAuthor(author);
  237.                 commit.setMessage(message);
  238.                 commit.setParentIds(parents);
  239.                 commit.setTreeId(indexTreeId);

  240.                 if (signCommit.booleanValue()) {
  241.                     sign(commit);
  242.                 }

  243.                 ObjectId commitId = odi.insert(commit);
  244.                 odi.flush();
  245.                 revCommit = rw.parseCommit(commitId);

  246.                 updateRef(state, headId, revCommit, commitId);
  247.             } finally {
  248.                 index.unlock();
  249.             }
  250.             try {
  251.                 Hooks.postCommit(repo, hookOutRedirect.get(PostCommitHook.NAME),
  252.                         hookErrRedirect.get(PostCommitHook.NAME)).call();
  253.             } catch (Exception e) {
  254.                 log.error(MessageFormat.format(
  255.                         JGitText.get().postCommitHookFailed, e.getMessage()),
  256.                         e);
  257.             }
  258.             return revCommit;
  259.         } catch (UnmergedPathException e) {
  260.             throw new UnmergedPathsException(e);
  261.         } catch (IOException e) {
  262.             throw new JGitInternalException(
  263.                     JGitText.get().exceptionCaughtDuringExecutionOfCommitCommand, e);
  264.         }
  265.     }

  266.     private void checkIfEmpty(RevWalk rw, ObjectId headId, ObjectId indexTreeId)
  267.             throws EmptyCommitException, MissingObjectException,
  268.             IncorrectObjectTypeException, IOException {
  269.         if (headId != null && !allowEmpty.booleanValue()) {
  270.             RevCommit headCommit = rw.parseCommit(headId);
  271.             headCommit.getTree();
  272.             if (indexTreeId.equals(headCommit.getTree())) {
  273.                 throw new EmptyCommitException(JGitText.get().emptyCommit);
  274.             }
  275.         }
  276.     }

  277.     private void sign(CommitBuilder commit) throws ServiceUnavailableException,
  278.             CanceledException, UnsupportedSigningFormatException {
  279.         if (gpgSigner == null) {
  280.             gpgSigner = GpgSigner.getDefault();
  281.             if (gpgSigner == null) {
  282.                 throw new ServiceUnavailableException(
  283.                         JGitText.get().signingServiceUnavailable);
  284.             }
  285.         }
  286.         if (signingKey == null) {
  287.             signingKey = gpgConfig.getSigningKey();
  288.         }
  289.         if (gpgSigner instanceof GpgObjectSigner) {
  290.             ((GpgObjectSigner) gpgSigner).signObject(commit,
  291.                     signingKey, committer, credentialsProvider,
  292.                     gpgConfig);
  293.         } else {
  294.             if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) {
  295.                 throw new UnsupportedSigningFormatException(JGitText
  296.                         .get().onlyOpenPgpSupportedForSigning);
  297.             }
  298.             gpgSigner.sign(commit, signingKey, committer,
  299.                     credentialsProvider);
  300.         }
  301.     }

  302.     private void updateRef(RepositoryState state, ObjectId headId,
  303.             RevCommit revCommit, ObjectId commitId)
  304.             throws ConcurrentRefUpdateException, IOException {
  305.         RefUpdate ru = repo.updateRef(Constants.HEAD);
  306.         ru.setNewObjectId(commitId);
  307.         if (!useDefaultReflogMessage) {
  308.             ru.setRefLogMessage(reflogComment, false);
  309.         } else {
  310.             String prefix = amend ? "commit (amend): " //$NON-NLS-1$
  311.                     : parents.isEmpty() ? "commit (initial): " //$NON-NLS-1$
  312.                             : "commit: "; //$NON-NLS-1$
  313.             ru.setRefLogMessage(prefix + revCommit.getShortMessage(),
  314.                     false);
  315.         }
  316.         if (headId != null) {
  317.             ru.setExpectedOldObjectId(headId);
  318.         } else {
  319.             ru.setExpectedOldObjectId(ObjectId.zeroId());
  320.         }
  321.         Result rc = ru.forceUpdate();
  322.         switch (rc) {
  323.         case NEW:
  324.         case FORCED:
  325.         case FAST_FORWARD: {
  326.             setCallable(false);
  327.             if (state == RepositoryState.MERGING_RESOLVED
  328.                     || isMergeDuringRebase(state)) {
  329.                 // Commit was successful. Now delete the files
  330.                 // used for merge commits
  331.                 repo.writeMergeCommitMsg(null);
  332.                 repo.writeMergeHeads(null);
  333.             } else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) {
  334.                 repo.writeMergeCommitMsg(null);
  335.                 repo.writeCherryPickHead(null);
  336.             } else if (state == RepositoryState.REVERTING_RESOLVED) {
  337.                 repo.writeMergeCommitMsg(null);
  338.                 repo.writeRevertHead(null);
  339.             }
  340.             break;
  341.         }
  342.         case REJECTED:
  343.         case LOCK_FAILURE:
  344.             throw new ConcurrentRefUpdateException(
  345.                     JGitText.get().couldNotLockHEAD, ru.getRef(), rc);
  346.         default:
  347.             throw new JGitInternalException(MessageFormat.format(
  348.                     JGitText.get().updatingRefFailed, Constants.HEAD,
  349.                     commitId.toString(), rc));
  350.         }
  351.     }

  352.     private void insertChangeId(ObjectId treeId) {
  353.         ObjectId firstParentId = null;
  354.         if (!parents.isEmpty())
  355.             firstParentId = parents.get(0);
  356.         ObjectId changeId = ChangeIdUtil.computeChangeId(treeId, firstParentId,
  357.                 author, committer, message);
  358.         message = ChangeIdUtil.insertId(message, changeId);
  359.         if (changeId != null)
  360.             message = message.replaceAll("\nChange-Id: I" //$NON-NLS-1$
  361.                     + ObjectId.zeroId().getName() + "\n", "\nChange-Id: I" //$NON-NLS-1$ //$NON-NLS-2$
  362.                     + changeId.getName() + "\n"); //$NON-NLS-1$
  363.     }

  364.     private DirCache createTemporaryIndex(ObjectId headId, DirCache index,
  365.             RevWalk rw)
  366.             throws IOException {
  367.         ObjectInserter inserter = null;

  368.         // get DirCacheBuilder for existing index
  369.         DirCacheBuilder existingBuilder = index.builder();

  370.         // get DirCacheBuilder for newly created in-core index to build a
  371.         // temporary index for this commit
  372.         DirCache inCoreIndex = DirCache.newInCore();
  373.         DirCacheBuilder tempBuilder = inCoreIndex.builder();

  374.         onlyProcessed = new boolean[only.size()];
  375.         boolean emptyCommit = true;

  376.         try (TreeWalk treeWalk = new TreeWalk(repo)) {
  377.             treeWalk.setOperationType(OperationType.CHECKIN_OP);
  378.             int dcIdx = treeWalk
  379.                     .addTree(new DirCacheBuildIterator(existingBuilder));
  380.             FileTreeIterator fti = new FileTreeIterator(repo);
  381.             fti.setDirCacheIterator(treeWalk, 0);
  382.             int fIdx = treeWalk.addTree(fti);
  383.             int hIdx = -1;
  384.             if (headId != null)
  385.                 hIdx = treeWalk.addTree(rw.parseTree(headId));
  386.             treeWalk.setRecursive(true);

  387.             String lastAddedFile = null;
  388.             while (treeWalk.next()) {
  389.                 String path = treeWalk.getPathString();
  390.                 // check if current entry's path matches a specified path
  391.                 int pos = lookupOnly(path);

  392.                 CanonicalTreeParser hTree = null;
  393.                 if (hIdx != -1)
  394.                     hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);

  395.                 DirCacheIterator dcTree = treeWalk.getTree(dcIdx,
  396.                         DirCacheIterator.class);

  397.                 if (pos >= 0) {
  398.                     // include entry in commit

  399.                     FileTreeIterator fTree = treeWalk.getTree(fIdx,
  400.                             FileTreeIterator.class);

  401.                     // check if entry refers to a tracked file
  402.                     boolean tracked = dcTree != null || hTree != null;
  403.                     if (!tracked)
  404.                         continue;

  405.                     // for an unmerged path, DirCacheBuildIterator will yield 3
  406.                     // entries, we only want to add one
  407.                     if (path.equals(lastAddedFile))
  408.                         continue;

  409.                     lastAddedFile = path;

  410.                     if (fTree != null) {
  411.                         // create a new DirCacheEntry with data retrieved from
  412.                         // disk
  413.                         final 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.                         boolean objectExists = (dcTree != null
  419.                                 && fTree.idEqual(dcTree))
  420.                                 || (hTree != null && fTree.idEqual(hTree));
  421.                         if (objectExists) {
  422.                             dcEntry.setObjectId(fTree.getEntryObjectId());
  423.                         } else {
  424.                             if (FileMode.GITLINK.equals(dcEntry.getFileMode()))
  425.                                 dcEntry.setObjectId(fTree.getEntryObjectId());
  426.                             else {
  427.                                 // insert object
  428.                                 if (inserter == null)
  429.                                     inserter = repo.newObjectInserter();
  430.                                 long contentLength = fTree
  431.                                         .getEntryContentLength();
  432.                                 try (InputStream inputStream = fTree
  433.                                         .openEntryStream()) {
  434.                                     dcEntry.setObjectId(inserter.insert(
  435.                                             Constants.OBJ_BLOB, contentLength,
  436.                                             inputStream));
  437.                                 }
  438.                             }
  439.                         }

  440.                         // add to existing index
  441.                         existingBuilder.add(dcEntry);
  442.                         // add to temporary in-core index
  443.                         tempBuilder.add(dcEntry);

  444.                         if (emptyCommit
  445.                                 && (hTree == null || !hTree.idEqual(fTree)
  446.                                         || hTree.getEntryRawMode() != fTree
  447.                                                 .getEntryRawMode()))
  448.                             // this is a change
  449.                             emptyCommit = false;
  450.                     } else {
  451.                         // if no file exists on disk, neither add it to
  452.                         // index nor to temporary in-core index

  453.                         if (emptyCommit && hTree != null)
  454.                             // this is a change
  455.                             emptyCommit = false;
  456.                     }

  457.                     // keep track of processed path
  458.                     onlyProcessed[pos] = true;
  459.                 } else {
  460.                     // add entries from HEAD for all other paths
  461.                     if (hTree != null) {
  462.                         // create a new DirCacheEntry with data retrieved from
  463.                         // HEAD
  464.                         final DirCacheEntry dcEntry = new DirCacheEntry(path);
  465.                         dcEntry.setObjectId(hTree.getEntryObjectId());
  466.                         dcEntry.setFileMode(hTree.getEntryFileMode());

  467.                         // add to temporary in-core index
  468.                         tempBuilder.add(dcEntry);
  469.                     }

  470.                     // preserve existing entry in index
  471.                     if (dcTree != null)
  472.                         existingBuilder.add(dcTree.getDirCacheEntry());
  473.                 }
  474.             }
  475.         }

  476.         // there must be no unprocessed paths left at this point; otherwise an
  477.         // untracked or unknown path has been specified
  478.         for (int i = 0; i < onlyProcessed.length; i++)
  479.             if (!onlyProcessed[i])
  480.                 throw new JGitInternalException(MessageFormat.format(
  481.                         JGitText.get().entryNotFoundByPath, only.get(i)));

  482.         // there must be at least one change
  483.         if (emptyCommit && !allowEmpty.booleanValue())
  484.             // Would like to throw a EmptyCommitException. But this would break the API
  485.             // TODO(ch): Change this in the next release
  486.             throw new JGitInternalException(JGitText.get().emptyCommit);

  487.         // update index
  488.         existingBuilder.commit();
  489.         // finish temporary in-core index used for this commit
  490.         tempBuilder.finish();
  491.         return inCoreIndex;
  492.     }

  493.     /**
  494.      * Look an entry's path up in the list of paths specified by the --only/ -o
  495.      * option
  496.      *
  497.      * In case the complete (file) path (e.g. "d1/d2/f1") cannot be found in
  498.      * <code>only</code>, lookup is also tried with (parent) directory paths
  499.      * (e.g. "d1/d2" and "d1").
  500.      *
  501.      * @param pathString
  502.      *            entry's path
  503.      * @return the item's index in <code>only</code>; -1 if no item matches
  504.      */
  505.     private int lookupOnly(String pathString) {
  506.         String p = pathString;
  507.         while (true) {
  508.             int position = Collections.binarySearch(only, p);
  509.             if (position >= 0)
  510.                 return position;
  511.             int l = p.lastIndexOf('/');
  512.             if (l < 1)
  513.                 break;
  514.             p = p.substring(0, l);
  515.         }
  516.         return -1;
  517.     }

  518.     /**
  519.      * Sets default values for not explicitly specified options. Then validates
  520.      * that all required data has been provided.
  521.      *
  522.      * @param state
  523.      *            the state of the repository we are working on
  524.      * @param rw
  525.      *            the RevWalk to use
  526.      *
  527.      * @throws NoMessageException
  528.      *             if the commit message has not been specified
  529.      * @throws UnsupportedSigningFormatException
  530.      *             if the configured gpg.format is not supported
  531.      */
  532.     private void processOptions(RepositoryState state, RevWalk rw)
  533.             throws NoMessageException, UnsupportedSigningFormatException {
  534.         if (committer == null)
  535.             committer = new PersonIdent(repo);
  536.         if (author == null && !amend)
  537.             author = committer;
  538.         if (allowEmpty == null)
  539.             // JGit allows empty commits by default. Only when pathes are
  540.             // specified the commit should not be empty. This behaviour differs
  541.             // from native git but can only be adapted in the next release.
  542.             // TODO(ch) align the defaults with native git
  543.             allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE;

  544.         // when doing a merge commit parse MERGE_HEAD and MERGE_MSG files
  545.         if (state == RepositoryState.MERGING_RESOLVED
  546.                 || isMergeDuringRebase(state)) {
  547.             try {
  548.                 parents = repo.readMergeHeads();
  549.                 if (parents != null)
  550.                     for (int i = 0; i < parents.size(); i++) {
  551.                         RevObject ro = rw.parseAny(parents.get(i));
  552.                         if (ro instanceof RevTag)
  553.                             parents.set(i, rw.peel(ro));
  554.                     }
  555.             } catch (IOException e) {
  556.                 throw new JGitInternalException(MessageFormat.format(
  557.                         JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
  558.                         Constants.MERGE_HEAD, e), e);
  559.             }
  560.             if (message == null) {
  561.                 try {
  562.                     message = repo.readMergeCommitMsg();
  563.                 } catch (IOException e) {
  564.                     throw new JGitInternalException(MessageFormat.format(
  565.                             JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
  566.                             Constants.MERGE_MSG, e), e);
  567.                 }
  568.             }
  569.         } else if (state == RepositoryState.SAFE && message == null) {
  570.             try {
  571.                 message = repo.readSquashCommitMsg();
  572.                 if (message != null)
  573.                     repo.writeSquashCommitMsg(null /* delete */);
  574.             } catch (IOException e) {
  575.                 throw new JGitInternalException(MessageFormat.format(
  576.                         JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
  577.                         Constants.MERGE_MSG, e), e);
  578.             }

  579.         }
  580.         if (message == null)
  581.             // as long as we don't support -C option we have to have
  582.             // an explicit message
  583.             throw new NoMessageException(JGitText.get().commitMessageNotSpecified);

  584.         if (gpgConfig == null) {
  585.             gpgConfig = new GpgConfig(repo.getConfig());
  586.         }
  587.         if (signCommit == null) {
  588.             signCommit = gpgConfig.isSignCommits() ? Boolean.TRUE
  589.                     : Boolean.FALSE;
  590.         }
  591.     }

  592.     private boolean isMergeDuringRebase(RepositoryState state) {
  593.         if (state != RepositoryState.REBASING_INTERACTIVE
  594.                 && state != RepositoryState.REBASING_MERGE)
  595.             return false;
  596.         try {
  597.             return repo.readMergeHeads() != null;
  598.         } catch (IOException e) {
  599.             throw new JGitInternalException(MessageFormat.format(
  600.                     JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
  601.                     Constants.MERGE_HEAD, e), e);
  602.         }
  603.     }

  604.     /**
  605.      * Set the commit message
  606.      *
  607.      * @param message
  608.      *            the commit message used for the {@code commit}
  609.      * @return {@code this}
  610.      */
  611.     public CommitCommand setMessage(String message) {
  612.         checkCallable();
  613.         this.message = message;
  614.         return this;
  615.     }

  616.     /**
  617.      * Sets the {@link CleanupMode} to apply to the commit message. If not
  618.      * called, {@link CommitCommand} applies {@link CleanupMode#VERBATIM}.
  619.      *
  620.      * @param mode
  621.      *            {@link CleanupMode} to set
  622.      * @return {@code this}
  623.      * @since 6.1
  624.      */
  625.     public CommitCommand setCleanupMode(@NonNull CleanupMode mode) {
  626.         checkCallable();
  627.         this.cleanupMode = mode;
  628.         return this;
  629.     }

  630.     /**
  631.      * Sets the default clean mode if {@link #setCleanupMode(CleanupMode)
  632.      * setCleanupMode(CleanupMode.DEFAULT)} is set and git config
  633.      * {@code commit.cleanup = default} or is not set.
  634.      *
  635.      * @param strip
  636.      *            if {@code true}, default to {@link CleanupMode#STRIP};
  637.      *            otherwise default to {@link CleanupMode#WHITESPACE}
  638.      * @return {@code this}
  639.      * @since 6.1
  640.      */
  641.     public CommitCommand setDefaultClean(boolean strip) {
  642.         checkCallable();
  643.         this.cleanDefaultIsStrip = strip;
  644.         return this;
  645.     }

  646.     /**
  647.      * Sets the comment character to apply when cleaning a commit message. If
  648.      * {@code null} (the default) and the {@link #setCleanupMode(CleanupMode)
  649.      * clean-up mode} is {@link CleanupMode#STRIP} or
  650.      * {@link CleanupMode#SCISSORS}, the value of git config
  651.      * {@code core.commentChar} will be used.
  652.      *
  653.      * @param commentChar
  654.      *            the comment character, or {@code null} to use the value from
  655.      *            the git config
  656.      * @return {@code this}
  657.      * @since 6.1
  658.      */
  659.     public CommitCommand setCommentCharacter(Character commentChar) {
  660.         checkCallable();
  661.         this.commentChar = commentChar;
  662.         return this;
  663.     }

  664.     /**
  665.      * Set whether to allow to create an empty commit
  666.      *
  667.      * @param allowEmpty
  668.      *            whether it should be allowed to create a commit which has the
  669.      *            same tree as it's sole predecessor (a commit which doesn't
  670.      *            change anything). By default when creating standard commits
  671.      *            (without specifying paths) JGit allows to create such commits.
  672.      *            When this flag is set to false an attempt to create an "empty"
  673.      *            standard commit will lead to an EmptyCommitException.
  674.      *            <p>
  675.      *            By default when creating a commit containing only specified
  676.      *            paths an attempt to create an empty commit leads to a
  677.      *            {@link org.eclipse.jgit.api.errors.JGitInternalException}. By
  678.      *            setting this flag to <code>true</code> this exception will not
  679.      *            be thrown.
  680.      * @return {@code this}
  681.      * @since 4.2
  682.      */
  683.     public CommitCommand setAllowEmpty(boolean allowEmpty) {
  684.         this.allowEmpty = Boolean.valueOf(allowEmpty);
  685.         return this;
  686.     }

  687.     /**
  688.      * Get the commit message
  689.      *
  690.      * @return the commit message used for the <code>commit</code>
  691.      */
  692.     public String getMessage() {
  693.         return message;
  694.     }

  695.     /**
  696.      * Sets the committer for this {@code commit}. If no committer is explicitly
  697.      * specified because this method is never called or called with {@code null}
  698.      * value then the committer will be deduced from config info in repository,
  699.      * with current time.
  700.      *
  701.      * @param committer
  702.      *            the committer used for the {@code commit}
  703.      * @return {@code this}
  704.      */
  705.     public CommitCommand setCommitter(PersonIdent committer) {
  706.         checkCallable();
  707.         this.committer = committer;
  708.         return this;
  709.     }

  710.     /**
  711.      * Sets the committer for this {@code commit}. If no committer is explicitly
  712.      * specified because this method is never called then the committer will be
  713.      * deduced from config info in repository, with current time.
  714.      *
  715.      * @param name
  716.      *            the name of the committer used for the {@code commit}
  717.      * @param email
  718.      *            the email of the committer used for the {@code commit}
  719.      * @return {@code this}
  720.      */
  721.     public CommitCommand setCommitter(String name, String email) {
  722.         checkCallable();
  723.         return setCommitter(new PersonIdent(name, email));
  724.     }

  725.     /**
  726.      * Get the committer
  727.      *
  728.      * @return the committer used for the {@code commit}. If no committer was
  729.      *         specified {@code null} is returned and the default
  730.      *         {@link org.eclipse.jgit.lib.PersonIdent} of this repo is used
  731.      *         during execution of the command
  732.      */
  733.     public PersonIdent getCommitter() {
  734.         return committer;
  735.     }

  736.     /**
  737.      * Sets the author for this {@code commit}. If no author is explicitly
  738.      * specified because this method is never called or called with {@code null}
  739.      * value then the author will be set to the committer or to the original
  740.      * author when amending.
  741.      *
  742.      * @param author
  743.      *            the author used for the {@code commit}
  744.      * @return {@code this}
  745.      */
  746.     public CommitCommand setAuthor(PersonIdent author) {
  747.         checkCallable();
  748.         this.author = author;
  749.         return this;
  750.     }

  751.     /**
  752.      * Sets the author for this {@code commit}. If no author is explicitly
  753.      * specified because this method is never called then the author will be set
  754.      * to the committer or to the original author when amending.
  755.      *
  756.      * @param name
  757.      *            the name of the author used for the {@code commit}
  758.      * @param email
  759.      *            the email of the author used for the {@code commit}
  760.      * @return {@code this}
  761.      */
  762.     public CommitCommand setAuthor(String name, String email) {
  763.         checkCallable();
  764.         return setAuthor(new PersonIdent(name, email));
  765.     }

  766.     /**
  767.      * Get the author
  768.      *
  769.      * @return the author used for the {@code commit}. If no author was
  770.      *         specified {@code null} is returned and the default
  771.      *         {@link org.eclipse.jgit.lib.PersonIdent} of this repo is used
  772.      *         during execution of the command
  773.      */
  774.     public PersonIdent getAuthor() {
  775.         return author;
  776.     }

  777.     /**
  778.      * If set to true the Commit command automatically stages files that have
  779.      * been modified and deleted, but new files not known by the repository are
  780.      * not affected. This corresponds to the parameter -a on the command line.
  781.      *
  782.      * @param all
  783.      *            whether to auto-stage all files that have been modified and
  784.      *            deleted
  785.      * @return {@code this}
  786.      * @throws JGitInternalException
  787.      *             in case of an illegal combination of arguments/ options
  788.      */
  789.     public CommitCommand setAll(boolean all) {
  790.         checkCallable();
  791.         if (all && !only.isEmpty())
  792.             throw new JGitInternalException(MessageFormat.format(
  793.                     JGitText.get().illegalCombinationOfArguments, "--all", //$NON-NLS-1$
  794.                     "--only")); //$NON-NLS-1$
  795.         this.all = all;
  796.         return this;
  797.     }

  798.     /**
  799.      * Used to amend the tip of the current branch. If set to {@code true}, the
  800.      * previous commit will be amended. This is equivalent to --amend on the
  801.      * command line.
  802.      *
  803.      * @param amend
  804.      *            whether to amend the tip of the current branch
  805.      * @return {@code this}
  806.      */
  807.     public CommitCommand setAmend(boolean amend) {
  808.         checkCallable();
  809.         this.amend = amend;
  810.         return this;
  811.     }

  812.     /**
  813.      * Commit dedicated path only.
  814.      * <p>
  815.      * This method can be called several times to add multiple paths. Full file
  816.      * paths are supported as well as directory paths; in the latter case this
  817.      * commits all files/directories below the specified path.
  818.      *
  819.      * @param only
  820.      *            path to commit (with <code>/</code> as separator)
  821.      * @return {@code this}
  822.      */
  823.     public CommitCommand setOnly(String only) {
  824.         checkCallable();
  825.         if (all)
  826.             throw new JGitInternalException(MessageFormat.format(
  827.                     JGitText.get().illegalCombinationOfArguments, "--only", //$NON-NLS-1$
  828.                     "--all")); //$NON-NLS-1$
  829.         String o = only.endsWith("/") ? only.substring(0, only.length() - 1) //$NON-NLS-1$
  830.                 : only;
  831.         // ignore duplicates
  832.         if (!this.only.contains(o))
  833.             this.only.add(o);
  834.         return this;
  835.     }

  836.     /**
  837.      * If set to true a change id will be inserted into the commit message
  838.      *
  839.      * An existing change id is not replaced. An initial change id (I000...)
  840.      * will be replaced by the change id.
  841.      *
  842.      * @param insertChangeId
  843.      *            whether to insert a change id
  844.      * @return {@code this}
  845.      */
  846.     public CommitCommand setInsertChangeId(boolean insertChangeId) {
  847.         checkCallable();
  848.         this.insertChangeId = insertChangeId;
  849.         return this;
  850.     }

  851.     /**
  852.      * Override the message written to the reflog
  853.      *
  854.      * @param reflogComment
  855.      *            the comment to be written into the reflog or <code>null</code>
  856.      *            to specify that no reflog should be written
  857.      * @return {@code this}
  858.      */
  859.     public CommitCommand setReflogComment(String reflogComment) {
  860.         this.reflogComment = reflogComment;
  861.         useDefaultReflogMessage = false;
  862.         return this;
  863.     }

  864.     /**
  865.      * Sets the {@link #noVerify} option on this commit command.
  866.      * <p>
  867.      * Both the pre-commit and commit-msg hooks can block a commit by their
  868.      * return value; setting this option to <code>true</code> will bypass these
  869.      * two hooks.
  870.      * </p>
  871.      *
  872.      * @param noVerify
  873.      *            Whether this commit should be verified by the pre-commit and
  874.      *            commit-msg hooks.
  875.      * @return {@code this}
  876.      * @since 3.7
  877.      */
  878.     public CommitCommand setNoVerify(boolean noVerify) {
  879.         this.noVerify = noVerify;
  880.         return this;
  881.     }

  882.     /**
  883.      * Set the output stream for all hook scripts executed by this command
  884.      * (pre-commit, commit-msg, post-commit). If not set it defaults to
  885.      * {@code System.out}.
  886.      *
  887.      * @param hookStdOut
  888.      *            the output stream for hook scripts executed by this command
  889.      * @return {@code this}
  890.      * @since 3.7
  891.      */
  892.     public CommitCommand setHookOutputStream(PrintStream hookStdOut) {
  893.         setHookOutputStream(PreCommitHook.NAME, hookStdOut);
  894.         setHookOutputStream(CommitMsgHook.NAME, hookStdOut);
  895.         setHookOutputStream(PostCommitHook.NAME, hookStdOut);
  896.         return this;
  897.     }

  898.     /**
  899.      * Set the error stream for all hook scripts executed by this command
  900.      * (pre-commit, commit-msg, post-commit). If not set it defaults to
  901.      * {@code System.err}.
  902.      *
  903.      * @param hookStdErr
  904.      *            the error stream for hook scripts executed by this command
  905.      * @return {@code this}
  906.      * @since 5.6
  907.      */
  908.     public CommitCommand setHookErrorStream(PrintStream hookStdErr) {
  909.         setHookErrorStream(PreCommitHook.NAME, hookStdErr);
  910.         setHookErrorStream(CommitMsgHook.NAME, hookStdErr);
  911.         setHookErrorStream(PostCommitHook.NAME, hookStdErr);
  912.         return this;
  913.     }

  914.     /**
  915.      * Set the output stream for a selected hook script executed by this command
  916.      * (pre-commit, commit-msg, post-commit). If not set it defaults to
  917.      * {@code System.out}.
  918.      *
  919.      * @param hookName
  920.      *            name of the hook to set the output stream for
  921.      * @param hookStdOut
  922.      *            the output stream to use for the selected hook
  923.      * @return {@code this}
  924.      * @since 4.5
  925.      */
  926.     public CommitCommand setHookOutputStream(String hookName,
  927.             PrintStream hookStdOut) {
  928.         if (!(PreCommitHook.NAME.equals(hookName)
  929.                 || CommitMsgHook.NAME.equals(hookName)
  930.                 || PostCommitHook.NAME.equals(hookName))) {
  931.             throw new IllegalArgumentException(
  932.                     MessageFormat.format(JGitText.get().illegalHookName,
  933.                             hookName));
  934.         }
  935.         hookOutRedirect.put(hookName, hookStdOut);
  936.         return this;
  937.     }

  938.     /**
  939.      * Set the error stream for a selected hook script executed by this command
  940.      * (pre-commit, commit-msg, post-commit). If not set it defaults to
  941.      * {@code System.err}.
  942.      *
  943.      * @param hookName
  944.      *            name of the hook to set the output stream for
  945.      * @param hookStdErr
  946.      *            the output stream to use for the selected hook
  947.      * @return {@code this}
  948.      * @since 5.6
  949.      */
  950.     public CommitCommand setHookErrorStream(String hookName,
  951.             PrintStream hookStdErr) {
  952.         if (!(PreCommitHook.NAME.equals(hookName)
  953.                 || CommitMsgHook.NAME.equals(hookName)
  954.                 || PostCommitHook.NAME.equals(hookName))) {
  955.             throw new IllegalArgumentException(MessageFormat
  956.                     .format(JGitText.get().illegalHookName, hookName));
  957.         }
  958.         hookErrRedirect.put(hookName, hookStdErr);
  959.         return this;
  960.     }

  961.     /**
  962.      * Sets the signing key
  963.      * <p>
  964.      * Per spec of user.signingKey: this will be sent to the GPG program as is,
  965.      * i.e. can be anything supported by the GPG program.
  966.      * </p>
  967.      * <p>
  968.      * Note, if none was set or <code>null</code> is specified a default will be
  969.      * obtained from the configuration.
  970.      * </p>
  971.      *
  972.      * @param signingKey
  973.      *            signing key (maybe <code>null</code>)
  974.      * @return {@code this}
  975.      * @since 5.3
  976.      */
  977.     public CommitCommand setSigningKey(String signingKey) {
  978.         checkCallable();
  979.         this.signingKey = signingKey;
  980.         return this;
  981.     }

  982.     /**
  983.      * Sets whether the commit should be signed.
  984.      *
  985.      * @param sign
  986.      *            <code>true</code> to sign, <code>false</code> to not sign and
  987.      *            <code>null</code> for default behavior (read from
  988.      *            configuration)
  989.      * @return {@code this}
  990.      * @since 5.3
  991.      */
  992.     public CommitCommand setSign(Boolean sign) {
  993.         checkCallable();
  994.         this.signCommit = sign;
  995.         return this;
  996.     }

  997.     /**
  998.      * Sets the {@link GpgSigner} to use if the commit is to be signed.
  999.      *
  1000.      * @param signer
  1001.      *            to use; if {@code null}, the default signer will be used
  1002.      * @return {@code this}
  1003.      * @since 5.11
  1004.      */
  1005.     public CommitCommand setGpgSigner(GpgSigner signer) {
  1006.         checkCallable();
  1007.         this.gpgSigner = signer;
  1008.         return this;
  1009.     }

  1010.     /**
  1011.      * Sets an external {@link GpgConfig} to use. Whether it will be used is at
  1012.      * the discretion of the {@link #setGpgSigner(GpgSigner)}.
  1013.      *
  1014.      * @param config
  1015.      *            to set; if {@code null}, the config will be loaded from the
  1016.      *            git config of the repository
  1017.      * @return {@code this}
  1018.      * @since 5.11
  1019.      */
  1020.     public CommitCommand setGpgConfig(GpgConfig config) {
  1021.         checkCallable();
  1022.         this.gpgConfig = config;
  1023.         return this;
  1024.     }

  1025.     /**
  1026.      * Sets a {@link CredentialsProvider}
  1027.      *
  1028.      * @param credentialsProvider
  1029.      *            the provider to use when querying for credentials (eg., during
  1030.      *            signing)
  1031.      * @return {@code this}
  1032.      * @since 6.0
  1033.      */
  1034.     public CommitCommand setCredentialsProvider(
  1035.             CredentialsProvider credentialsProvider) {
  1036.         this.credentialsProvider = credentialsProvider;
  1037.         return this;
  1038.     }
  1039. }