| /* | |
| * Copyright (C) 2010-2012, Christian Halstrick <christian.halstrick@sap.com> 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.List; | |
| import org.eclipse.jgit.annotations.NonNull; | |
| 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.CommitConfig; | |
| import org.eclipse.jgit.lib.CommitConfig.CleanupMode; | |
| import org.eclipse.jgit.lib.Constants; | |
| import org.eclipse.jgit.lib.FileMode; | |
| import org.eclipse.jgit.lib.GpgConfig; | |
| 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.lib.Signer; | |
| import org.eclipse.jgit.lib.Signers; | |
| 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 <a | |
| * href="http://www.kernel.org/pub/software/scm/git/docs/git-commit.html" | |
| * >Git documentation about Commit</a> | |
| */ | |
| public class CommitCommand extends GitCommand<RevCommit> { | |
| private static final Logger log = LoggerFactory | |
| .getLogger(CommitCommand.class); | |
| private PersonIdent author; | |
| private PersonIdent committer; | |
| private String message; | |
| private boolean all; | |
| private List<String> 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<ObjectId> parents = new ArrayList<>(); | |
| private String reflogComment; | |
| private boolean useDefaultReflogMessage = true; | |
| /** | |
| * Setting this option bypasses the pre-commit and commit-msg hooks. | |
| */ | |
| private boolean noVerify; | |
| private HashMap<String, PrintStream> hookOutRedirect = new HashMap<>(3); | |
| private HashMap<String, PrintStream> hookErrRedirect = new HashMap<>(3); | |
| private Boolean allowEmpty; | |
| private Boolean signCommit; | |
| private String signingKey; | |
| private Signer signer; | |
| private GpgConfig gpgConfig; | |
| private CredentialsProvider credentialsProvider; | |
| private @NonNull CleanupMode cleanupMode = CleanupMode.VERBATIM; | |
| private boolean cleanDefaultIsStrip = true; | |
| private Character commentChar; | |
| /** | |
| * Constructor for CommitCommand | |
| * | |
| * @param repo | |
| * the {@link org.eclipse.jgit.lib.Repository} | |
| */ | |
| protected CommitCommand(Repository repo) { | |
| super(repo); | |
| this.credentialsProvider = CredentialsProvider.getDefault(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| * <p> | |
| * 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(); | |
| } | |
| CommitConfig config = null; | |
| if (CleanupMode.DEFAULT.equals(cleanupMode)) { | |
| config = repo.getConfig().get(CommitConfig.KEY); | |
| cleanupMode = config.resolve(cleanupMode, cleanDefaultIsStrip); | |
| } | |
| char comments = (char) 0; | |
| if (CleanupMode.STRIP.equals(cleanupMode) | |
| || CleanupMode.SCISSORS.equals(cleanupMode)) { | |
| if (commentChar == null) { | |
| if (config == null) { | |
| config = repo.getConfig().get(CommitConfig.KEY); | |
| } | |
| if (config.isAutoCommentChar()) { | |
| // We're supposed to pick a character that isn't used, | |
| // but then cleaning up won't remove any lines. So don't | |
| // bother. | |
| comments = (char) 0; | |
| cleanupMode = CleanupMode.WHITESPACE; | |
| } else { | |
| comments = config.getCommentChar(); | |
| } | |
| } else { | |
| comments = commentChar.charValue(); | |
| } | |
| } | |
| message = CommitConfig.cleanText(message, cleanupMode, comments); | |
| 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 CanceledException, IOException, | |
| UnsupportedSigningFormatException { | |
| if (signer == null) { | |
| signer = Signers.get(gpgConfig.getKeyFormat()); | |
| if (signer == null) { | |
| throw new UnsupportedSigningFormatException(MessageFormat | |
| .format(JGitText.get().signatureTypeUnknown, | |
| gpgConfig.getKeyFormat().toConfigValue())); | |
| } | |
| } | |
| if (signingKey == null) { | |
| signingKey = gpgConfig.getSigningKey(); | |
| } | |
| signer.signObject(repo, gpgConfig, commit, committer, signingKey, | |
| 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 | |
| * <code>only</code>, 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 <code>only</code>; -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; | |
| } | |
| } | |
| 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; | |
| } | |
| /** | |
| * Sets the {@link CleanupMode} to apply to the commit message. If not | |
| * called, {@link CommitCommand} applies {@link CleanupMode#VERBATIM}. | |
| * | |
| * @param mode | |
| * {@link CleanupMode} to set | |
| * @return {@code this} | |
| * @since 6.1 | |
| */ | |
| public CommitCommand setCleanupMode(@NonNull CleanupMode mode) { | |
| checkCallable(); | |
| this.cleanupMode = mode; | |
| return this; | |
| } | |
| /** | |
| * Sets the default clean mode if {@link #setCleanupMode(CleanupMode) | |
| * setCleanupMode(CleanupMode.DEFAULT)} is set and git config | |
| * {@code commit.cleanup = default} or is not set. | |
| * | |
| * @param strip | |
| * if {@code true}, default to {@link CleanupMode#STRIP}; | |
| * otherwise default to {@link CleanupMode#WHITESPACE} | |
| * @return {@code this} | |
| * @since 6.1 | |
| */ | |
| public CommitCommand setDefaultClean(boolean strip) { | |
| checkCallable(); | |
| this.cleanDefaultIsStrip = strip; | |
| return this; | |
| } | |
| /** | |
| * Sets the comment character to apply when cleaning a commit message. If | |
| * {@code null} (the default) and the {@link #setCleanupMode(CleanupMode) | |
| * clean-up mode} is {@link CleanupMode#STRIP} or | |
| * {@link CleanupMode#SCISSORS}, the value of git config | |
| * {@code core.commentChar} will be used. | |
| * | |
| * @param commentChar | |
| * the comment character, or {@code null} to use the value from | |
| * the git config | |
| * @return {@code this} | |
| * @since 6.1 | |
| */ | |
| public CommitCommand setCommentCharacter(Character commentChar) { | |
| checkCallable(); | |
| this.commentChar = commentChar; | |
| 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. | |
| * <p> | |
| * 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 <code>true</code> 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 <code>commit</code> | |
| */ | |
| 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 amend the tip of the current branch | |
| * @return {@code this} | |
| */ | |
| public CommitCommand setAmend(boolean amend) { | |
| checkCallable(); | |
| this.amend = amend; | |
| return this; | |
| } | |
| /** | |
| * Commit dedicated path only. | |
| * <p> | |
| * 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 <code>/</code> 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 <code>null</code> | |
| * 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. | |
| * <p> | |
| * Both the pre-commit and commit-msg hooks can block a commit by their | |
| * return value; setting this option to <code>true</code> will bypass these | |
| * two hooks. | |
| * </p> | |
| * | |
| * @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 | |
| * <p> | |
| * 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. | |
| * </p> | |
| * <p> | |
| * Note, if none was set or <code>null</code> is specified a default will be | |
| * obtained from the configuration. | |
| * </p> | |
| * | |
| * @param signingKey | |
| * signing key (maybe <code>null</code>) | |
| * @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 | |
| * <code>true</code> to sign, <code>false</code> to not sign and | |
| * <code>null</code> 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 Signer} 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 7.0 | |
| */ | |
| public CommitCommand setSigner(Signer signer) { | |
| checkCallable(); | |
| this.signer = signer; | |
| return this; | |
| } | |
| /** | |
| * Sets an external {@link GpgConfig} to use. Whether it will be used is at | |
| * the discretion of the {@link #setSigner(Signer)}. | |
| * | |
| * @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; | |
| } | |
| } |