/* * Copyright (C) 2010-2012, Christian Halstrick and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.api; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.EmptyCommitException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoFilepatternException; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.api.errors.NoMessageException; import org.eclipse.jgit.api.errors.ServiceUnavailableException; import org.eclipse.jgit.api.errors.UnmergedPathsException; import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuildIterator; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.UnmergedPathException; import org.eclipse.jgit.hooks.CommitMsgHook; import org.eclipse.jgit.hooks.Hooks; import org.eclipse.jgit.hooks.PostCommitHook; import org.eclipse.jgit.hooks.PreCommitHook; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.GpgConfig; import org.eclipse.jgit.lib.GpgConfig.GpgFormat; import org.eclipse.jgit.lib.GpgObjectSigner; import org.eclipse.jgit.lib.GpgSigner; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevTag; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk.OperationType; import org.eclipse.jgit.util.ChangeIdUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A class used to execute a {@code Commit} command. It has setters for all * supported options and arguments of this command and a {@link #call()} method * to finally execute the command. * * @see Git documentation about Commit */ public class CommitCommand extends GitCommand { private static final Logger log = LoggerFactory .getLogger(CommitCommand.class); private PersonIdent author; private PersonIdent committer; private String message; private boolean all; private List only = new ArrayList<>(); private boolean[] onlyProcessed; private boolean amend; private boolean insertChangeId; /** * parents this commit should have. The current HEAD will be in this list * and also all commits mentioned in .git/MERGE_HEAD */ private List parents = new LinkedList<>(); private String reflogComment; private boolean useDefaultReflogMessage = true; /** * Setting this option bypasses the pre-commit and commit-msg hooks. */ private boolean noVerify; private HashMap hookOutRedirect = new HashMap<>(3); private HashMap hookErrRedirect = new HashMap<>(3); private Boolean allowEmpty; private Boolean signCommit; private String signingKey; private GpgSigner gpgSigner; private GpgConfig gpgConfig; private CredentialsProvider credentialsProvider; /** * Constructor for CommitCommand * * @param repo * the {@link org.eclipse.jgit.lib.Repository} */ protected CommitCommand(Repository repo) { super(repo); this.credentialsProvider = CredentialsProvider.getDefault(); } /** * {@inheritDoc} *

* Executes the {@code commit} command with all the options and parameters * collected by the setter methods of this class. Each instance of this * class should only be used for one invocation of the command (means: one * call to {@link #call()}) * * @throws ServiceUnavailableException * if signing service is not available e.g. since it isn't * installed */ @Override public RevCommit call() throws GitAPIException, AbortedByHookException, ConcurrentRefUpdateException, NoHeadException, NoMessageException, ServiceUnavailableException, UnmergedPathsException, WrongRepositoryStateException { checkCallable(); Collections.sort(only); try (RevWalk rw = new RevWalk(repo)) { RepositoryState state = repo.getRepositoryState(); if (!state.canCommit()) throw new WrongRepositoryStateException(MessageFormat.format( JGitText.get().cannotCommitOnARepoWithState, state.name())); if (!noVerify) { Hooks.preCommit(repo, hookOutRedirect.get(PreCommitHook.NAME), hookErrRedirect.get(PreCommitHook.NAME)) .call(); } processOptions(state, rw); if (all && !repo.isBare()) { try (Git git = new Git(repo)) { git.add().addFilepattern(".") //$NON-NLS-1$ .setUpdate(true).call(); } catch (NoFilepatternException e) { // should really not happen throw new JGitInternalException(e.getMessage(), e); } } Ref head = repo.exactRef(Constants.HEAD); if (head == null) throw new NoHeadException( JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported); // determine the current HEAD and the commit it is referring to ObjectId headId = repo.resolve(Constants.HEAD + "^{commit}"); //$NON-NLS-1$ if (headId == null && amend) throw new WrongRepositoryStateException( JGitText.get().commitAmendOnInitialNotPossible); if (headId != null) if (amend) { RevCommit previousCommit = rw.parseCommit(headId); for (RevCommit p : previousCommit.getParents()) parents.add(p.getId()); if (author == null) author = previousCommit.getAuthorIdent(); } else { parents.add(0, headId); } if (!noVerify) { message = Hooks .commitMsg(repo, hookOutRedirect.get(CommitMsgHook.NAME), hookErrRedirect.get(CommitMsgHook.NAME)) .setCommitMessage(message).call(); } RevCommit revCommit; DirCache index = repo.lockDirCache(); try (ObjectInserter odi = repo.newObjectInserter()) { if (!only.isEmpty()) index = createTemporaryIndex(headId, index, rw); // Write the index as tree to the object database. This may // fail for example when the index contains unmerged paths // (unresolved conflicts) ObjectId indexTreeId = index.writeTree(odi); if (insertChangeId) insertChangeId(indexTreeId); checkIfEmpty(rw, headId, indexTreeId); // Create a Commit object, populate it and write it CommitBuilder commit = new CommitBuilder(); commit.setCommitter(committer); commit.setAuthor(author); commit.setMessage(message); commit.setParentIds(parents); commit.setTreeId(indexTreeId); if (signCommit.booleanValue()) { sign(commit); } ObjectId commitId = odi.insert(commit); odi.flush(); revCommit = rw.parseCommit(commitId); updateRef(state, headId, revCommit, commitId); } finally { index.unlock(); } try { Hooks.postCommit(repo, hookOutRedirect.get(PostCommitHook.NAME), hookErrRedirect.get(PostCommitHook.NAME)).call(); } catch (Exception e) { log.error(MessageFormat.format( JGitText.get().postCommitHookFailed, e.getMessage()), e); } return revCommit; } catch (UnmergedPathException e) { throw new UnmergedPathsException(e); } catch (IOException e) { throw new JGitInternalException( JGitText.get().exceptionCaughtDuringExecutionOfCommitCommand, e); } } private void checkIfEmpty(RevWalk rw, ObjectId headId, ObjectId indexTreeId) throws EmptyCommitException, MissingObjectException, IncorrectObjectTypeException, IOException { if (headId != null && !allowEmpty.booleanValue()) { RevCommit headCommit = rw.parseCommit(headId); headCommit.getTree(); if (indexTreeId.equals(headCommit.getTree())) { throw new EmptyCommitException(JGitText.get().emptyCommit); } } } private void sign(CommitBuilder commit) throws ServiceUnavailableException, CanceledException, UnsupportedSigningFormatException { if (gpgSigner == null) { throw new ServiceUnavailableException( JGitText.get().signingServiceUnavailable); } if (gpgSigner instanceof GpgObjectSigner) { ((GpgObjectSigner) gpgSigner).signObject(commit, signingKey, committer, credentialsProvider, gpgConfig); } else { if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) { throw new UnsupportedSigningFormatException(JGitText .get().onlyOpenPgpSupportedForSigning); } gpgSigner.sign(commit, signingKey, committer, credentialsProvider); } } private void updateRef(RepositoryState state, ObjectId headId, RevCommit revCommit, ObjectId commitId) throws ConcurrentRefUpdateException, IOException { RefUpdate ru = repo.updateRef(Constants.HEAD); ru.setNewObjectId(commitId); if (!useDefaultReflogMessage) { ru.setRefLogMessage(reflogComment, false); } else { String prefix = amend ? "commit (amend): " //$NON-NLS-1$ : parents.isEmpty() ? "commit (initial): " //$NON-NLS-1$ : "commit: "; //$NON-NLS-1$ ru.setRefLogMessage(prefix + revCommit.getShortMessage(), false); } if (headId != null) { ru.setExpectedOldObjectId(headId); } else { ru.setExpectedOldObjectId(ObjectId.zeroId()); } Result rc = ru.forceUpdate(); switch (rc) { case NEW: case FORCED: case FAST_FORWARD: { setCallable(false); if (state == RepositoryState.MERGING_RESOLVED || isMergeDuringRebase(state)) { // Commit was successful. Now delete the files // used for merge commits repo.writeMergeCommitMsg(null); repo.writeMergeHeads(null); } else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) { repo.writeMergeCommitMsg(null); repo.writeCherryPickHead(null); } else if (state == RepositoryState.REVERTING_RESOLVED) { repo.writeMergeCommitMsg(null); repo.writeRevertHead(null); } break; } case REJECTED: case LOCK_FAILURE: throw new ConcurrentRefUpdateException( JGitText.get().couldNotLockHEAD, ru.getRef(), rc); default: throw new JGitInternalException(MessageFormat.format( JGitText.get().updatingRefFailed, Constants.HEAD, commitId.toString(), rc)); } } private void insertChangeId(ObjectId treeId) { ObjectId firstParentId = null; if (!parents.isEmpty()) firstParentId = parents.get(0); ObjectId changeId = ChangeIdUtil.computeChangeId(treeId, firstParentId, author, committer, message); message = ChangeIdUtil.insertId(message, changeId); if (changeId != null) message = message.replaceAll("\nChange-Id: I" //$NON-NLS-1$ + ObjectId.zeroId().getName() + "\n", "\nChange-Id: I" //$NON-NLS-1$ //$NON-NLS-2$ + changeId.getName() + "\n"); //$NON-NLS-1$ } private DirCache createTemporaryIndex(ObjectId headId, DirCache index, RevWalk rw) throws IOException { ObjectInserter inserter = null; // get DirCacheBuilder for existing index DirCacheBuilder existingBuilder = index.builder(); // get DirCacheBuilder for newly created in-core index to build a // temporary index for this commit DirCache inCoreIndex = DirCache.newInCore(); DirCacheBuilder tempBuilder = inCoreIndex.builder(); onlyProcessed = new boolean[only.size()]; boolean emptyCommit = true; try (TreeWalk treeWalk = new TreeWalk(repo)) { treeWalk.setOperationType(OperationType.CHECKIN_OP); int dcIdx = treeWalk .addTree(new DirCacheBuildIterator(existingBuilder)); FileTreeIterator fti = new FileTreeIterator(repo); fti.setDirCacheIterator(treeWalk, 0); int fIdx = treeWalk.addTree(fti); int hIdx = -1; if (headId != null) hIdx = treeWalk.addTree(rw.parseTree(headId)); treeWalk.setRecursive(true); String lastAddedFile = null; while (treeWalk.next()) { String path = treeWalk.getPathString(); // check if current entry's path matches a specified path int pos = lookupOnly(path); CanonicalTreeParser hTree = null; if (hIdx != -1) hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class); DirCacheIterator dcTree = treeWalk.getTree(dcIdx, DirCacheIterator.class); if (pos >= 0) { // include entry in commit FileTreeIterator fTree = treeWalk.getTree(fIdx, FileTreeIterator.class); // check if entry refers to a tracked file boolean tracked = dcTree != null || hTree != null; if (!tracked) continue; // for an unmerged path, DirCacheBuildIterator will yield 3 // entries, we only want to add one if (path.equals(lastAddedFile)) continue; lastAddedFile = path; if (fTree != null) { // create a new DirCacheEntry with data retrieved from // disk final DirCacheEntry dcEntry = new DirCacheEntry(path); long entryLength = fTree.getEntryLength(); dcEntry.setLength(entryLength); dcEntry.setLastModified(fTree.getEntryLastModifiedInstant()); dcEntry.setFileMode(fTree.getIndexFileMode(dcTree)); boolean objectExists = (dcTree != null && fTree.idEqual(dcTree)) || (hTree != null && fTree.idEqual(hTree)); if (objectExists) { dcEntry.setObjectId(fTree.getEntryObjectId()); } else { if (FileMode.GITLINK.equals(dcEntry.getFileMode())) dcEntry.setObjectId(fTree.getEntryObjectId()); else { // insert object if (inserter == null) inserter = repo.newObjectInserter(); long contentLength = fTree .getEntryContentLength(); try (InputStream inputStream = fTree .openEntryStream()) { dcEntry.setObjectId(inserter.insert( Constants.OBJ_BLOB, contentLength, inputStream)); } } } // add to existing index existingBuilder.add(dcEntry); // add to temporary in-core index tempBuilder.add(dcEntry); if (emptyCommit && (hTree == null || !hTree.idEqual(fTree) || hTree.getEntryRawMode() != fTree .getEntryRawMode())) // this is a change emptyCommit = false; } else { // if no file exists on disk, neither add it to // index nor to temporary in-core index if (emptyCommit && hTree != null) // this is a change emptyCommit = false; } // keep track of processed path onlyProcessed[pos] = true; } else { // add entries from HEAD for all other paths if (hTree != null) { // create a new DirCacheEntry with data retrieved from // HEAD final DirCacheEntry dcEntry = new DirCacheEntry(path); dcEntry.setObjectId(hTree.getEntryObjectId()); dcEntry.setFileMode(hTree.getEntryFileMode()); // add to temporary in-core index tempBuilder.add(dcEntry); } // preserve existing entry in index if (dcTree != null) existingBuilder.add(dcTree.getDirCacheEntry()); } } } // there must be no unprocessed paths left at this point; otherwise an // untracked or unknown path has been specified for (int i = 0; i < onlyProcessed.length; i++) if (!onlyProcessed[i]) throw new JGitInternalException(MessageFormat.format( JGitText.get().entryNotFoundByPath, only.get(i))); // there must be at least one change if (emptyCommit && !allowEmpty.booleanValue()) // Would like to throw a EmptyCommitException. But this would break the API // TODO(ch): Change this in the next release throw new JGitInternalException(JGitText.get().emptyCommit); // update index existingBuilder.commit(); // finish temporary in-core index used for this commit tempBuilder.finish(); return inCoreIndex; } /** * Look an entry's path up in the list of paths specified by the --only/ -o * option * * In case the complete (file) path (e.g. "d1/d2/f1") cannot be found in * only, lookup is also tried with (parent) directory paths * (e.g. "d1/d2" and "d1"). * * @param pathString * entry's path * @return the item's index in only; -1 if no item matches */ private int lookupOnly(String pathString) { String p = pathString; while (true) { int position = Collections.binarySearch(only, p); if (position >= 0) return position; int l = p.lastIndexOf('/'); if (l < 1) break; p = p.substring(0, l); } return -1; } /** * Sets default values for not explicitly specified options. Then validates * that all required data has been provided. * * @param state * the state of the repository we are working on * @param rw * the RevWalk to use * * @throws NoMessageException * if the commit message has not been specified * @throws UnsupportedSigningFormatException * if the configured gpg.format is not supported */ private void processOptions(RepositoryState state, RevWalk rw) throws NoMessageException, UnsupportedSigningFormatException { if (committer == null) committer = new PersonIdent(repo); if (author == null && !amend) author = committer; if (allowEmpty == null) // JGit allows empty commits by default. Only when pathes are // specified the commit should not be empty. This behaviour differs // from native git but can only be adapted in the next release. // TODO(ch) align the defaults with native git allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE; // when doing a merge commit parse MERGE_HEAD and MERGE_MSG files if (state == RepositoryState.MERGING_RESOLVED || isMergeDuringRebase(state)) { try { parents = repo.readMergeHeads(); if (parents != null) for (int i = 0; i < parents.size(); i++) { RevObject ro = rw.parseAny(parents.get(i)); if (ro instanceof RevTag) parents.set(i, rw.peel(ro)); } } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR, Constants.MERGE_HEAD, e), e); } if (message == null) { try { message = repo.readMergeCommitMsg(); } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR, Constants.MERGE_MSG, e), e); } } } else if (state == RepositoryState.SAFE && message == null) { try { message = repo.readSquashCommitMsg(); if (message != null) repo.writeSquashCommitMsg(null /* delete */); } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR, Constants.MERGE_MSG, e), e); } } if (message == null) // as long as we don't support -C option we have to have // an explicit message throw new NoMessageException(JGitText.get().commitMessageNotSpecified); if (gpgConfig == null) { gpgConfig = new GpgConfig(repo.getConfig()); } if (signCommit == null) { signCommit = gpgConfig.isSignCommits() ? Boolean.TRUE : Boolean.FALSE; } if (signingKey == null) { signingKey = gpgConfig.getSigningKey(); } if (gpgSigner == null) { gpgSigner = GpgSigner.getDefault(); } } private boolean isMergeDuringRebase(RepositoryState state) { if (state != RepositoryState.REBASING_INTERACTIVE && state != RepositoryState.REBASING_MERGE) return false; try { return repo.readMergeHeads() != null; } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR, Constants.MERGE_HEAD, e), e); } } /** * Set the commit message * * @param message * the commit message used for the {@code commit} * @return {@code this} */ public CommitCommand setMessage(String message) { checkCallable(); this.message = message; return this; } /** * Set whether to allow to create an empty commit * * @param allowEmpty * whether it should be allowed to create a commit which has the * same tree as it's sole predecessor (a commit which doesn't * change anything). By default when creating standard commits * (without specifying paths) JGit allows to create such commits. * When this flag is set to false an attempt to create an "empty" * standard commit will lead to an EmptyCommitException. *

* By default when creating a commit containing only specified * paths an attempt to create an empty commit leads to a * {@link org.eclipse.jgit.api.errors.JGitInternalException}. By * setting this flag to true this exception will not * be thrown. * @return {@code this} * @since 4.2 */ public CommitCommand setAllowEmpty(boolean allowEmpty) { this.allowEmpty = Boolean.valueOf(allowEmpty); return this; } /** * Get the commit message * * @return the commit message used for the commit */ public String getMessage() { return message; } /** * Sets the committer for this {@code commit}. If no committer is explicitly * specified because this method is never called or called with {@code null} * value then the committer will be deduced from config info in repository, * with current time. * * @param committer * the committer used for the {@code commit} * @return {@code this} */ public CommitCommand setCommitter(PersonIdent committer) { checkCallable(); this.committer = committer; return this; } /** * Sets the committer for this {@code commit}. If no committer is explicitly * specified because this method is never called then the committer will be * deduced from config info in repository, with current time. * * @param name * the name of the committer used for the {@code commit} * @param email * the email of the committer used for the {@code commit} * @return {@code this} */ public CommitCommand setCommitter(String name, String email) { checkCallable(); return setCommitter(new PersonIdent(name, email)); } /** * Get the committer * * @return the committer used for the {@code commit}. If no committer was * specified {@code null} is returned and the default * {@link org.eclipse.jgit.lib.PersonIdent} of this repo is used * during execution of the command */ public PersonIdent getCommitter() { return committer; } /** * Sets the author for this {@code commit}. If no author is explicitly * specified because this method is never called or called with {@code null} * value then the author will be set to the committer or to the original * author when amending. * * @param author * the author used for the {@code commit} * @return {@code this} */ public CommitCommand setAuthor(PersonIdent author) { checkCallable(); this.author = author; return this; } /** * Sets the author for this {@code commit}. If no author is explicitly * specified because this method is never called then the author will be set * to the committer or to the original author when amending. * * @param name * the name of the author used for the {@code commit} * @param email * the email of the author used for the {@code commit} * @return {@code this} */ public CommitCommand setAuthor(String name, String email) { checkCallable(); return setAuthor(new PersonIdent(name, email)); } /** * Get the author * * @return the author used for the {@code commit}. If no author was * specified {@code null} is returned and the default * {@link org.eclipse.jgit.lib.PersonIdent} of this repo is used * during execution of the command */ public PersonIdent getAuthor() { return author; } /** * If set to true the Commit command automatically stages files that have * been modified and deleted, but new files not known by the repository are * not affected. This corresponds to the parameter -a on the command line. * * @param all * whether to auto-stage all files that have been modified and * deleted * @return {@code this} * @throws JGitInternalException * in case of an illegal combination of arguments/ options */ public CommitCommand setAll(boolean all) { checkCallable(); if (all && !only.isEmpty()) throw new JGitInternalException(MessageFormat.format( JGitText.get().illegalCombinationOfArguments, "--all", //$NON-NLS-1$ "--only")); //$NON-NLS-1$ this.all = all; return this; } /** * Used to amend the tip of the current branch. If set to {@code true}, the * previous commit will be amended. This is equivalent to --amend on the * command line. * * @param amend * whether to ammend the tip of the current branch * @return {@code this} */ public CommitCommand setAmend(boolean amend) { checkCallable(); this.amend = amend; return this; } /** * Commit dedicated path only. *

* This method can be called several times to add multiple paths. Full file * paths are supported as well as directory paths; in the latter case this * commits all files/directories below the specified path. * * @param only * path to commit (with / as separator) * @return {@code this} */ public CommitCommand setOnly(String only) { checkCallable(); if (all) throw new JGitInternalException(MessageFormat.format( JGitText.get().illegalCombinationOfArguments, "--only", //$NON-NLS-1$ "--all")); //$NON-NLS-1$ String o = only.endsWith("/") ? only.substring(0, only.length() - 1) //$NON-NLS-1$ : only; // ignore duplicates if (!this.only.contains(o)) this.only.add(o); return this; } /** * If set to true a change id will be inserted into the commit message * * An existing change id is not replaced. An initial change id (I000...) * will be replaced by the change id. * * @param insertChangeId * whether to insert a change id * @return {@code this} */ public CommitCommand setInsertChangeId(boolean insertChangeId) { checkCallable(); this.insertChangeId = insertChangeId; return this; } /** * Override the message written to the reflog * * @param reflogComment * the comment to be written into the reflog or null * to specify that no reflog should be written * @return {@code this} */ public CommitCommand setReflogComment(String reflogComment) { this.reflogComment = reflogComment; useDefaultReflogMessage = false; return this; } /** * Sets the {@link #noVerify} option on this commit command. *

* Both the pre-commit and commit-msg hooks can block a commit by their * return value; setting this option to true will bypass these * two hooks. *

* * @param noVerify * Whether this commit should be verified by the pre-commit and * commit-msg hooks. * @return {@code this} * @since 3.7 */ public CommitCommand setNoVerify(boolean noVerify) { this.noVerify = noVerify; return this; } /** * Set the output stream for all hook scripts executed by this command * (pre-commit, commit-msg, post-commit). If not set it defaults to * {@code System.out}. * * @param hookStdOut * the output stream for hook scripts executed by this command * @return {@code this} * @since 3.7 */ public CommitCommand setHookOutputStream(PrintStream hookStdOut) { setHookOutputStream(PreCommitHook.NAME, hookStdOut); setHookOutputStream(CommitMsgHook.NAME, hookStdOut); setHookOutputStream(PostCommitHook.NAME, hookStdOut); return this; } /** * Set the error stream for all hook scripts executed by this command * (pre-commit, commit-msg, post-commit). If not set it defaults to * {@code System.err}. * * @param hookStdErr * the error stream for hook scripts executed by this command * @return {@code this} * @since 5.6 */ public CommitCommand setHookErrorStream(PrintStream hookStdErr) { setHookErrorStream(PreCommitHook.NAME, hookStdErr); setHookErrorStream(CommitMsgHook.NAME, hookStdErr); setHookErrorStream(PostCommitHook.NAME, hookStdErr); return this; } /** * Set the output stream for a selected hook script executed by this command * (pre-commit, commit-msg, post-commit). If not set it defaults to * {@code System.out}. * * @param hookName * name of the hook to set the output stream for * @param hookStdOut * the output stream to use for the selected hook * @return {@code this} * @since 4.5 */ public CommitCommand setHookOutputStream(String hookName, PrintStream hookStdOut) { if (!(PreCommitHook.NAME.equals(hookName) || CommitMsgHook.NAME.equals(hookName) || PostCommitHook.NAME.equals(hookName))) { throw new IllegalArgumentException( MessageFormat.format(JGitText.get().illegalHookName, hookName)); } hookOutRedirect.put(hookName, hookStdOut); return this; } /** * Set the error stream for a selected hook script executed by this command * (pre-commit, commit-msg, post-commit). If not set it defaults to * {@code System.err}. * * @param hookName * name of the hook to set the output stream for * @param hookStdErr * the output stream to use for the selected hook * @return {@code this} * @since 5.6 */ public CommitCommand setHookErrorStream(String hookName, PrintStream hookStdErr) { if (!(PreCommitHook.NAME.equals(hookName) || CommitMsgHook.NAME.equals(hookName) || PostCommitHook.NAME.equals(hookName))) { throw new IllegalArgumentException(MessageFormat .format(JGitText.get().illegalHookName, hookName)); } hookErrRedirect.put(hookName, hookStdErr); return this; } /** * Sets the signing key *

* Per spec of user.signingKey: this will be sent to the GPG program as is, * i.e. can be anything supported by the GPG program. *

*

* Note, if none was set or null is specified a default will be * obtained from the configuration. *

* * @param signingKey * signing key (maybe null) * @return {@code this} * @since 5.3 */ public CommitCommand setSigningKey(String signingKey) { checkCallable(); this.signingKey = signingKey; return this; } /** * Sets whether the commit should be signed. * * @param sign * true to sign, false to not sign and * null for default behavior (read from * configuration) * @return {@code this} * @since 5.3 */ public CommitCommand setSign(Boolean sign) { checkCallable(); this.signCommit = sign; return this; } /** * Sets the {@link GpgSigner} to use if the commit is to be signed. * * @param signer * to use; if {@code null}, the default signer will be used * @return {@code this} * @since 5.11 */ public CommitCommand setGpgSigner(GpgSigner signer) { checkCallable(); this.gpgSigner = signer; return this; } /** * Sets an external {@link GpgConfig} to use. Whether it will be used is at * the discretion of the {@link #setGpgSigner(GpgSigner)}. * * @param config * to set; if {@code null}, the config will be loaded from the * git config of the repository * @return {@code this} * @since 5.11 */ public CommitCommand setGpgConfig(GpgConfig config) { checkCallable(); this.gpgConfig = config; return this; } /** * Sets a {@link CredentialsProvider} * * @param credentialsProvider * the provider to use when querying for credentials (eg., during * signing) * @return {@code this} * @since 6.0 */ public CommitCommand setCredentialsProvider( CredentialsProvider credentialsProvider) { this.credentialsProvider = credentialsProvider; return this; } }