View Javadoc
1   /*
2    * Copyright (C) 2010-2012, Christian Halstrick <christian.halstrick@sap.com>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.api;
44  
45  import java.io.IOException;
46  import java.io.InputStream;
47  import java.io.PrintStream;
48  import java.text.MessageFormat;
49  import java.util.ArrayList;
50  import java.util.Collections;
51  import java.util.HashMap;
52  import java.util.LinkedList;
53  import java.util.List;
54  
55  import org.eclipse.jgit.api.errors.AbortedByHookException;
56  import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
57  import org.eclipse.jgit.api.errors.EmptyCommitException;
58  import org.eclipse.jgit.api.errors.GitAPIException;
59  import org.eclipse.jgit.api.errors.JGitInternalException;
60  import org.eclipse.jgit.api.errors.NoFilepatternException;
61  import org.eclipse.jgit.api.errors.NoHeadException;
62  import org.eclipse.jgit.api.errors.NoMessageException;
63  import org.eclipse.jgit.api.errors.UnmergedPathsException;
64  import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
65  import org.eclipse.jgit.dircache.DirCache;
66  import org.eclipse.jgit.dircache.DirCacheBuildIterator;
67  import org.eclipse.jgit.dircache.DirCacheBuilder;
68  import org.eclipse.jgit.dircache.DirCacheEntry;
69  import org.eclipse.jgit.dircache.DirCacheIterator;
70  import org.eclipse.jgit.errors.UnmergedPathException;
71  import org.eclipse.jgit.hooks.CommitMsgHook;
72  import org.eclipse.jgit.hooks.Hooks;
73  import org.eclipse.jgit.hooks.PostCommitHook;
74  import org.eclipse.jgit.hooks.PreCommitHook;
75  import org.eclipse.jgit.internal.JGitText;
76  import org.eclipse.jgit.lib.CommitBuilder;
77  import org.eclipse.jgit.lib.Constants;
78  import org.eclipse.jgit.lib.FileMode;
79  import org.eclipse.jgit.lib.ObjectId;
80  import org.eclipse.jgit.lib.ObjectInserter;
81  import org.eclipse.jgit.lib.PersonIdent;
82  import org.eclipse.jgit.lib.Ref;
83  import org.eclipse.jgit.lib.RefUpdate;
84  import org.eclipse.jgit.lib.RefUpdate.Result;
85  import org.eclipse.jgit.lib.Repository;
86  import org.eclipse.jgit.lib.RepositoryState;
87  import org.eclipse.jgit.revwalk.RevCommit;
88  import org.eclipse.jgit.revwalk.RevObject;
89  import org.eclipse.jgit.revwalk.RevTag;
90  import org.eclipse.jgit.revwalk.RevWalk;
91  import org.eclipse.jgit.treewalk.CanonicalTreeParser;
92  import org.eclipse.jgit.treewalk.FileTreeIterator;
93  import org.eclipse.jgit.treewalk.TreeWalk;
94  import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
95  import org.eclipse.jgit.util.ChangeIdUtil;
96  
97  /**
98   * A class used to execute a {@code Commit} command. It has setters for all
99   * supported options and arguments of this command and a {@link #call()} method
100  * to finally execute the command.
101  *
102  * @see <a
103  *      href="http://www.kernel.org/pub/software/scm/git/docs/git-commit.html"
104  *      >Git documentation about Commit</a>
105  */
106 public class CommitCommand extends GitCommand<RevCommit> {
107 	private PersonIdent author;
108 
109 	private PersonIdent committer;
110 
111 	private String message;
112 
113 	private boolean all;
114 
115 	private List<String> only = new ArrayList<>();
116 
117 	private boolean[] onlyProcessed;
118 
119 	private boolean amend;
120 
121 	private boolean insertChangeId;
122 
123 	/**
124 	 * parents this commit should have. The current HEAD will be in this list
125 	 * and also all commits mentioned in .git/MERGE_HEAD
126 	 */
127 	private List<ObjectId> parents = new LinkedList<>();
128 
129 	private String reflogComment;
130 
131 	private boolean useDefaultReflogMessage = true;
132 
133 	/**
134 	 * Setting this option bypasses the pre-commit and commit-msg hooks.
135 	 */
136 	private boolean noVerify;
137 
138 	private HashMap<String, PrintStream> hookOutRedirect = new HashMap<>(3);
139 
140 	private Boolean allowEmpty;
141 
142 	/**
143 	 * Constructor for CommitCommand
144 	 *
145 	 * @param repo
146 	 *            the {@link org.eclipse.jgit.lib.Repository}
147 	 */
148 	protected CommitCommand(Repository repo) {
149 		super(repo);
150 	}
151 
152 	/**
153 	 * {@inheritDoc}
154 	 * <p>
155 	 * Executes the {@code commit} command with all the options and parameters
156 	 * collected by the setter methods of this class. Each instance of this
157 	 * class should only be used for one invocation of the command (means: one
158 	 * call to {@link #call()})
159 	 */
160 	@Override
161 	public RevCommit call() throws GitAPIException, NoHeadException,
162 			NoMessageException, UnmergedPathsException,
163 			ConcurrentRefUpdateException, WrongRepositoryStateException,
164 			AbortedByHookException {
165 		checkCallable();
166 		Collections.sort(only);
167 
168 		try (RevWalk rw = new RevWalk(repo)) {
169 			RepositoryState state = repo.getRepositoryState();
170 			if (!state.canCommit())
171 				throw new WrongRepositoryStateException(MessageFormat.format(
172 						JGitText.get().cannotCommitOnARepoWithState,
173 						state.name()));
174 
175 			if (!noVerify) {
176 				Hooks.preCommit(repo, hookOutRedirect.get(PreCommitHook.NAME))
177 						.call();
178 			}
179 
180 			processOptions(state, rw);
181 
182 			if (all && !repo.isBare()) {
183 				try (Git git = new Git(repo)) {
184 					git.add()
185 							.addFilepattern(".") //$NON-NLS-1$
186 							.setUpdate(true).call();
187 				} catch (NoFilepatternException e) {
188 					// should really not happen
189 					throw new JGitInternalException(e.getMessage(), e);
190 				}
191 			}
192 
193 			Ref head = repo.exactRef(Constants.HEAD);
194 			if (head == null)
195 				throw new NoHeadException(
196 						JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
197 
198 			// determine the current HEAD and the commit it is referring to
199 			ObjectId headId = repo.resolve(Constants.HEAD + "^{commit}"); //$NON-NLS-1$
200 			if (headId == null && amend)
201 				throw new WrongRepositoryStateException(
202 						JGitText.get().commitAmendOnInitialNotPossible);
203 
204 			if (headId != null)
205 				if (amend) {
206 					RevCommit previousCommit = rw.parseCommit(headId);
207 					for (RevCommit p : previousCommit.getParents())
208 						parents.add(p.getId());
209 					if (author == null)
210 						author = previousCommit.getAuthorIdent();
211 				} else {
212 					parents.add(0, headId);
213 				}
214 
215 			if (!noVerify) {
216 				message = Hooks
217 						.commitMsg(repo,
218 								hookOutRedirect.get(CommitMsgHook.NAME))
219 						.setCommitMessage(message).call();
220 			}
221 
222 			// lock the index
223 			DirCache index = repo.lockDirCache();
224 			try (ObjectInserter odi = repo.newObjectInserter()) {
225 				if (!only.isEmpty())
226 					index = createTemporaryIndex(headId, index, rw);
227 
228 				// Write the index as tree to the object database. This may
229 				// fail for example when the index contains unmerged paths
230 				// (unresolved conflicts)
231 				ObjectId indexTreeId = index.writeTree(odi);
232 
233 				if (insertChangeId)
234 					insertChangeId(indexTreeId);
235 
236 				// Check for empty commits
237 				if (headId != null && !allowEmpty.booleanValue()) {
238 					RevCommit headCommit = rw.parseCommit(headId);
239 					headCommit.getTree();
240 					if (indexTreeId.equals(headCommit.getTree())) {
241 						throw new EmptyCommitException(
242 								JGitText.get().emptyCommit);
243 					}
244 				}
245 
246 				// Create a Commit object, populate it and write it
247 				CommitBuilder commit = new CommitBuilder();
248 				commit.setCommitter(committer);
249 				commit.setAuthor(author);
250 				commit.setMessage(message);
251 
252 				commit.setParentIds(parents);
253 				commit.setTreeId(indexTreeId);
254 				ObjectId commitId = odi.insert(commit);
255 				odi.flush();
256 
257 				RevCommit revCommit = rw.parseCommit(commitId);
258 				RefUpdate ru = repo.updateRef(Constants.HEAD);
259 				ru.setNewObjectId(commitId);
260 				if (!useDefaultReflogMessage) {
261 					ru.setRefLogMessage(reflogComment, false);
262 				} else {
263 					String prefix = amend ? "commit (amend): " //$NON-NLS-1$
264 							: parents.size() == 0 ? "commit (initial): " //$NON-NLS-1$
265 									: "commit: "; //$NON-NLS-1$
266 					ru.setRefLogMessage(prefix + revCommit.getShortMessage(),
267 							false);
268 				}
269 				if (headId != null)
270 					ru.setExpectedOldObjectId(headId);
271 				else
272 					ru.setExpectedOldObjectId(ObjectId.zeroId());
273 				Result rc = ru.forceUpdate();
274 				switch (rc) {
275 				case NEW:
276 				case FORCED:
277 				case FAST_FORWARD: {
278 					setCallable(false);
279 					if (state == RepositoryState.MERGING_RESOLVED
280 							|| isMergeDuringRebase(state)) {
281 						// Commit was successful. Now delete the files
282 						// used for merge commits
283 						repo.writeMergeCommitMsg(null);
284 						repo.writeMergeHeads(null);
285 					} else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) {
286 						repo.writeMergeCommitMsg(null);
287 						repo.writeCherryPickHead(null);
288 					} else if (state == RepositoryState.REVERTING_RESOLVED) {
289 						repo.writeMergeCommitMsg(null);
290 						repo.writeRevertHead(null);
291 					}
292 					Hooks.postCommit(repo,
293 							hookOutRedirect.get(PostCommitHook.NAME)).call();
294 
295 					return revCommit;
296 				}
297 				case REJECTED:
298 				case LOCK_FAILURE:
299 					throw new ConcurrentRefUpdateException(
300 							JGitText.get().couldNotLockHEAD, ru.getRef(), rc);
301 				default:
302 					throw new JGitInternalException(MessageFormat.format(
303 							JGitText.get().updatingRefFailed, Constants.HEAD,
304 							commitId.toString(), rc));
305 				}
306 			} finally {
307 				index.unlock();
308 			}
309 		} catch (UnmergedPathException e) {
310 			throw new UnmergedPathsException(e);
311 		} catch (IOException e) {
312 			throw new JGitInternalException(
313 					JGitText.get().exceptionCaughtDuringExecutionOfCommitCommand, e);
314 		}
315 	}
316 
317 	private void insertChangeId(ObjectId treeId) {
318 		ObjectId firstParentId = null;
319 		if (!parents.isEmpty())
320 			firstParentId = parents.get(0);
321 		ObjectId changeId = ChangeIdUtil.computeChangeId(treeId, firstParentId,
322 				author, committer, message);
323 		message = ChangeIdUtil.insertId(message, changeId);
324 		if (changeId != null)
325 			message = message.replaceAll("\nChange-Id: I" //$NON-NLS-1$
326 					+ ObjectId.zeroId().getName() + "\n", "\nChange-Id: I" //$NON-NLS-1$ //$NON-NLS-2$
327 					+ changeId.getName() + "\n"); //$NON-NLS-1$
328 	}
329 
330 	private DirCache createTemporaryIndex(ObjectId headId, DirCache index,
331 			RevWalk rw)
332 			throws IOException {
333 		ObjectInserter inserter = null;
334 
335 		// get DirCacheBuilder for existing index
336 		DirCacheBuilder existingBuilder = index.builder();
337 
338 		// get DirCacheBuilder for newly created in-core index to build a
339 		// temporary index for this commit
340 		DirCache inCoreIndex = DirCache.newInCore();
341 		DirCacheBuilder tempBuilder = inCoreIndex.builder();
342 
343 		onlyProcessed = new boolean[only.size()];
344 		boolean emptyCommit = true;
345 
346 		try (TreeWalk treeWalk = new TreeWalk(repo)) {
347 			treeWalk.setOperationType(OperationType.CHECKIN_OP);
348 			int dcIdx = treeWalk
349 					.addTree(new DirCacheBuildIterator(existingBuilder));
350 			FileTreeIterator fti = new FileTreeIterator(repo);
351 			fti.setDirCacheIterator(treeWalk, 0);
352 			int fIdx = treeWalk.addTree(fti);
353 			int hIdx = -1;
354 			if (headId != null)
355 				hIdx = treeWalk.addTree(rw.parseTree(headId));
356 			treeWalk.setRecursive(true);
357 
358 			String lastAddedFile = null;
359 			while (treeWalk.next()) {
360 				String path = treeWalk.getPathString();
361 				// check if current entry's path matches a specified path
362 				int pos = lookupOnly(path);
363 
364 				CanonicalTreeParser hTree = null;
365 				if (hIdx != -1)
366 					hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
367 
368 				DirCacheIterator dcTree = treeWalk.getTree(dcIdx,
369 						DirCacheIterator.class);
370 
371 				if (pos >= 0) {
372 					// include entry in commit
373 
374 					FileTreeIterator fTree = treeWalk.getTree(fIdx,
375 							FileTreeIterator.class);
376 
377 					// check if entry refers to a tracked file
378 					boolean tracked = dcTree != null || hTree != null;
379 					if (!tracked)
380 						continue;
381 
382 					// for an unmerged path, DirCacheBuildIterator will yield 3
383 					// entries, we only want to add one
384 					if (path.equals(lastAddedFile))
385 						continue;
386 
387 					lastAddedFile = path;
388 
389 					if (fTree != null) {
390 						// create a new DirCacheEntry with data retrieved from
391 						// disk
392 						final DirCacheEntry dcEntry = new DirCacheEntry(path);
393 						long entryLength = fTree.getEntryLength();
394 						dcEntry.setLength(entryLength);
395 						dcEntry.setLastModified(fTree.getEntryLastModified());
396 						dcEntry.setFileMode(fTree.getIndexFileMode(dcTree));
397 
398 						boolean objectExists = (dcTree != null
399 								&& fTree.idEqual(dcTree))
400 								|| (hTree != null && fTree.idEqual(hTree));
401 						if (objectExists) {
402 							dcEntry.setObjectId(fTree.getEntryObjectId());
403 						} else {
404 							if (FileMode.GITLINK.equals(dcEntry.getFileMode()))
405 								dcEntry.setObjectId(fTree.getEntryObjectId());
406 							else {
407 								// insert object
408 								if (inserter == null)
409 									inserter = repo.newObjectInserter();
410 								long contentLength = fTree
411 										.getEntryContentLength();
412 								try (InputStream inputStream = fTree
413 										.openEntryStream()) {
414 									dcEntry.setObjectId(inserter.insert(
415 											Constants.OBJ_BLOB, contentLength,
416 											inputStream));
417 								}
418 							}
419 						}
420 
421 						// add to existing index
422 						existingBuilder.add(dcEntry);
423 						// add to temporary in-core index
424 						tempBuilder.add(dcEntry);
425 
426 						if (emptyCommit
427 								&& (hTree == null || !hTree.idEqual(fTree)
428 										|| hTree.getEntryRawMode() != fTree
429 												.getEntryRawMode()))
430 							// this is a change
431 							emptyCommit = false;
432 					} else {
433 						// if no file exists on disk, neither add it to
434 						// index nor to temporary in-core index
435 
436 						if (emptyCommit && hTree != null)
437 							// this is a change
438 							emptyCommit = false;
439 					}
440 
441 					// keep track of processed path
442 					onlyProcessed[pos] = true;
443 				} else {
444 					// add entries from HEAD for all other paths
445 					if (hTree != null) {
446 						// create a new DirCacheEntry with data retrieved from
447 						// HEAD
448 						final DirCacheEntry dcEntry = new DirCacheEntry(path);
449 						dcEntry.setObjectId(hTree.getEntryObjectId());
450 						dcEntry.setFileMode(hTree.getEntryFileMode());
451 
452 						// add to temporary in-core index
453 						tempBuilder.add(dcEntry);
454 					}
455 
456 					// preserve existing entry in index
457 					if (dcTree != null)
458 						existingBuilder.add(dcTree.getDirCacheEntry());
459 				}
460 			}
461 		}
462 
463 		// there must be no unprocessed paths left at this point; otherwise an
464 		// untracked or unknown path has been specified
465 		for (int i = 0; i < onlyProcessed.length; i++)
466 			if (!onlyProcessed[i])
467 				throw new JGitInternalException(MessageFormat.format(
468 						JGitText.get().entryNotFoundByPath, only.get(i)));
469 
470 		// there must be at least one change
471 		if (emptyCommit && !allowEmpty.booleanValue())
472 			// Would like to throw a EmptyCommitException. But this would break the API
473 			// TODO(ch): Change this in the next release
474 			throw new JGitInternalException(JGitText.get().emptyCommit);
475 
476 		// update index
477 		existingBuilder.commit();
478 		// finish temporary in-core index used for this commit
479 		tempBuilder.finish();
480 		return inCoreIndex;
481 	}
482 
483 	/**
484 	 * Look an entry's path up in the list of paths specified by the --only/ -o
485 	 * option
486 	 *
487 	 * In case the complete (file) path (e.g. "d1/d2/f1") cannot be found in
488 	 * <code>only</code>, lookup is also tried with (parent) directory paths
489 	 * (e.g. "d1/d2" and "d1").
490 	 *
491 	 * @param pathString
492 	 *            entry's path
493 	 * @return the item's index in <code>only</code>; -1 if no item matches
494 	 */
495 	private int lookupOnly(String pathString) {
496 		String p = pathString;
497 		while (true) {
498 			int position = Collections.binarySearch(only, p);
499 			if (position >= 0)
500 				return position;
501 			int l = p.lastIndexOf("/"); //$NON-NLS-1$
502 			if (l < 1)
503 				break;
504 			p = p.substring(0, l);
505 		}
506 		return -1;
507 	}
508 
509 	/**
510 	 * Sets default values for not explicitly specified options. Then validates
511 	 * that all required data has been provided.
512 	 *
513 	 * @param state
514 	 *            the state of the repository we are working on
515 	 * @param rw
516 	 *            the RevWalk to use
517 	 *
518 	 * @throws NoMessageException
519 	 *             if the commit message has not been specified
520 	 */
521 	private void processOptions(RepositoryState state, RevWalk rw)
522 			throws NoMessageException {
523 		if (committer == null)
524 			committer = new PersonIdent(repo);
525 		if (author == null && !amend)
526 			author = committer;
527 		if (allowEmpty == null)
528 			// JGit allows empty commits by default. Only when pathes are
529 			// specified the commit should not be empty. This behaviour differs
530 			// from native git but can only be adapted in the next release.
531 			// TODO(ch) align the defaults with native git
532 			allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE;
533 
534 		// when doing a merge commit parse MERGE_HEAD and MERGE_MSG files
535 		if (state == RepositoryState.MERGING_RESOLVED
536 				|| isMergeDuringRebase(state)) {
537 			try {
538 				parents = repo.readMergeHeads();
539 				if (parents != null)
540 					for (int i = 0; i < parents.size(); i++) {
541 						RevObject ro = rw.parseAny(parents.get(i));
542 						if (ro instanceof RevTag)
543 							parents.set(i, rw.peel(ro));
544 					}
545 			} catch (IOException e) {
546 				throw new JGitInternalException(MessageFormat.format(
547 						JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
548 						Constants.MERGE_HEAD, e), e);
549 			}
550 			if (message == null) {
551 				try {
552 					message = repo.readMergeCommitMsg();
553 				} catch (IOException e) {
554 					throw new JGitInternalException(MessageFormat.format(
555 							JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
556 							Constants.MERGE_MSG, e), e);
557 				}
558 			}
559 		} else if (state == RepositoryState.SAFE && message == null) {
560 			try {
561 				message = repo.readSquashCommitMsg();
562 				if (message != null)
563 					repo.writeSquashCommitMsg(null /* delete */);
564 			} catch (IOException e) {
565 				throw new JGitInternalException(MessageFormat.format(
566 						JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
567 						Constants.MERGE_MSG, e), e);
568 			}
569 
570 		}
571 		if (message == null)
572 			// as long as we don't support -C option we have to have
573 			// an explicit message
574 			throw new NoMessageException(JGitText.get().commitMessageNotSpecified);
575 	}
576 
577 	private boolean isMergeDuringRebase(RepositoryState state) {
578 		if (state != RepositoryState.REBASING_INTERACTIVE
579 				&& state != RepositoryState.REBASING_MERGE)
580 			return false;
581 		try {
582 			return repo.readMergeHeads() != null;
583 		} catch (IOException e) {
584 			throw new JGitInternalException(MessageFormat.format(
585 					JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
586 					Constants.MERGE_HEAD, e), e);
587 		}
588 	}
589 
590 	/**
591 	 * Set the commit message
592 	 *
593 	 * @param message
594 	 *            the commit message used for the {@code commit}
595 	 * @return {@code this}
596 	 */
597 	public CommitCommand setMessage(String message) {
598 		checkCallable();
599 		this.message = message;
600 		return this;
601 	}
602 
603 	/**
604 	 * Set whether to allow to create an empty commit
605 	 *
606 	 * @param allowEmpty
607 	 *            whether it should be allowed to create a commit which has the
608 	 *            same tree as it's sole predecessor (a commit which doesn't
609 	 *            change anything). By default when creating standard commits
610 	 *            (without specifying paths) JGit allows to create such commits.
611 	 *            When this flag is set to false an attempt to create an "empty"
612 	 *            standard commit will lead to an EmptyCommitException.
613 	 *            <p>
614 	 *            By default when creating a commit containing only specified
615 	 *            paths an attempt to create an empty commit leads to a
616 	 *            {@link org.eclipse.jgit.api.errors.JGitInternalException}. By
617 	 *            setting this flag to <code>true</code> this exception will not
618 	 *            be thrown.
619 	 * @return {@code this}
620 	 * @since 4.2
621 	 */
622 	public CommitCommand setAllowEmpty(boolean allowEmpty) {
623 		this.allowEmpty = Boolean.valueOf(allowEmpty);
624 		return this;
625 	}
626 
627 	/**
628 	 * Get the commit message
629 	 *
630 	 * @return the commit message used for the <code>commit</code>
631 	 */
632 	public String getMessage() {
633 		return message;
634 	}
635 
636 	/**
637 	 * Sets the committer for this {@code commit}. If no committer is explicitly
638 	 * specified because this method is never called or called with {@code null}
639 	 * value then the committer will be deduced from config info in repository,
640 	 * with current time.
641 	 *
642 	 * @param committer
643 	 *            the committer used for the {@code commit}
644 	 * @return {@code this}
645 	 */
646 	public CommitCommand setCommitter(PersonIdent committer) {
647 		checkCallable();
648 		this.committer = committer;
649 		return this;
650 	}
651 
652 	/**
653 	 * Sets the committer for this {@code commit}. If no committer is explicitly
654 	 * specified because this method is never called then the committer will be
655 	 * deduced from config info in repository, with current time.
656 	 *
657 	 * @param name
658 	 *            the name of the committer used for the {@code commit}
659 	 * @param email
660 	 *            the email of the committer used for the {@code commit}
661 	 * @return {@code this}
662 	 */
663 	public CommitCommand setCommitter(String name, String email) {
664 		checkCallable();
665 		return setCommitter(new PersonIdent(name, email));
666 	}
667 
668 	/**
669 	 * Get the committer
670 	 *
671 	 * @return the committer used for the {@code commit}. If no committer was
672 	 *         specified {@code null} is returned and the default
673 	 *         {@link org.eclipse.jgit.lib.PersonIdent} of this repo is used
674 	 *         during execution of the command
675 	 */
676 	public PersonIdent getCommitter() {
677 		return committer;
678 	}
679 
680 	/**
681 	 * Sets the author for this {@code commit}. If no author is explicitly
682 	 * specified because this method is never called or called with {@code null}
683 	 * value then the author will be set to the committer or to the original
684 	 * author when amending.
685 	 *
686 	 * @param author
687 	 *            the author used for the {@code commit}
688 	 * @return {@code this}
689 	 */
690 	public CommitCommand setAuthor(PersonIdent author) {
691 		checkCallable();
692 		this.author = author;
693 		return this;
694 	}
695 
696 	/**
697 	 * Sets the author for this {@code commit}. If no author is explicitly
698 	 * specified because this method is never called then the author will be set
699 	 * to the committer or to the original author when amending.
700 	 *
701 	 * @param name
702 	 *            the name of the author used for the {@code commit}
703 	 * @param email
704 	 *            the email of the author used for the {@code commit}
705 	 * @return {@code this}
706 	 */
707 	public CommitCommand setAuthor(String name, String email) {
708 		checkCallable();
709 		return setAuthor(new PersonIdent(name, email));
710 	}
711 
712 	/**
713 	 * Get the author
714 	 *
715 	 * @return the author used for the {@code commit}. If no author was
716 	 *         specified {@code null} is returned and the default
717 	 *         {@link org.eclipse.jgit.lib.PersonIdent} of this repo is used
718 	 *         during execution of the command
719 	 */
720 	public PersonIdent getAuthor() {
721 		return author;
722 	}
723 
724 	/**
725 	 * If set to true the Commit command automatically stages files that have
726 	 * been modified and deleted, but new files not known by the repository are
727 	 * not affected. This corresponds to the parameter -a on the command line.
728 	 *
729 	 * @param all
730 	 *            whether to auto-stage all files that have been modified and
731 	 *            deleted
732 	 * @return {@code this}
733 	 * @throws JGitInternalException
734 	 *             in case of an illegal combination of arguments/ options
735 	 */
736 	public CommitCommand setAll(boolean all) {
737 		checkCallable();
738 		if (all && !only.isEmpty())
739 			throw new JGitInternalException(MessageFormat.format(
740 					JGitText.get().illegalCombinationOfArguments, "--all", //$NON-NLS-1$
741 					"--only")); //$NON-NLS-1$
742 		this.all = all;
743 		return this;
744 	}
745 
746 	/**
747 	 * Used to amend the tip of the current branch. If set to {@code true}, the
748 	 * previous commit will be amended. This is equivalent to --amend on the
749 	 * command line.
750 	 *
751 	 * @param amend
752 	 *            whether to ammend the tip of the current branch
753 	 * @return {@code this}
754 	 */
755 	public CommitCommand setAmend(boolean amend) {
756 		checkCallable();
757 		this.amend = amend;
758 		return this;
759 	}
760 
761 	/**
762 	 * Commit dedicated path only.
763 	 * <p>
764 	 * This method can be called several times to add multiple paths. Full file
765 	 * paths are supported as well as directory paths; in the latter case this
766 	 * commits all files/directories below the specified path.
767 	 *
768 	 * @param only
769 	 *            path to commit (with <code>/</code> as separator)
770 	 * @return {@code this}
771 	 */
772 	public CommitCommand setOnly(String only) {
773 		checkCallable();
774 		if (all)
775 			throw new JGitInternalException(MessageFormat.format(
776 					JGitText.get().illegalCombinationOfArguments, "--only", //$NON-NLS-1$
777 					"--all")); //$NON-NLS-1$
778 		String o = only.endsWith("/") ? only.substring(0, only.length() - 1) //$NON-NLS-1$
779 				: only;
780 		// ignore duplicates
781 		if (!this.only.contains(o))
782 			this.only.add(o);
783 		return this;
784 	}
785 
786 	/**
787 	 * If set to true a change id will be inserted into the commit message
788 	 *
789 	 * An existing change id is not replaced. An initial change id (I000...)
790 	 * will be replaced by the change id.
791 	 *
792 	 * @param insertChangeId
793 	 *            whether to insert a change id
794 	 * @return {@code this}
795 	 */
796 	public CommitCommand setInsertChangeId(boolean insertChangeId) {
797 		checkCallable();
798 		this.insertChangeId = insertChangeId;
799 		return this;
800 	}
801 
802 	/**
803 	 * Override the message written to the reflog
804 	 *
805 	 * @param reflogComment
806 	 *            the comment to be written into the reflog or <code>null</code>
807 	 *            to specify that no reflog should be written
808 	 * @return {@code this}
809 	 */
810 	public CommitCommand setReflogComment(String reflogComment) {
811 		this.reflogComment = reflogComment;
812 		useDefaultReflogMessage = false;
813 		return this;
814 	}
815 
816 	/**
817 	 * Sets the {@link #noVerify} option on this commit command.
818 	 * <p>
819 	 * Both the pre-commit and commit-msg hooks can block a commit by their
820 	 * return value; setting this option to <code>true</code> will bypass these
821 	 * two hooks.
822 	 * </p>
823 	 *
824 	 * @param noVerify
825 	 *            Whether this commit should be verified by the pre-commit and
826 	 *            commit-msg hooks.
827 	 * @return {@code this}
828 	 * @since 3.7
829 	 */
830 	public CommitCommand setNoVerify(boolean noVerify) {
831 		this.noVerify = noVerify;
832 		return this;
833 	}
834 
835 	/**
836 	 * Set the output stream for all hook scripts executed by this command
837 	 * (pre-commit, commit-msg, post-commit). If not set it defaults to
838 	 * {@code System.out}.
839 	 *
840 	 * @param hookStdOut
841 	 *            the output stream for hook scripts executed by this command
842 	 * @return {@code this}
843 	 * @since 3.7
844 	 */
845 	public CommitCommand setHookOutputStream(PrintStream hookStdOut) {
846 		setHookOutputStream(PreCommitHook.NAME, hookStdOut);
847 		setHookOutputStream(CommitMsgHook.NAME, hookStdOut);
848 		setHookOutputStream(PostCommitHook.NAME, hookStdOut);
849 		return this;
850 	}
851 
852 	/**
853 	 * Set the output stream for a selected hook script executed by this command
854 	 * (pre-commit, commit-msg, post-commit). If not set it defaults to
855 	 * {@code System.out}.
856 	 *
857 	 * @param hookName
858 	 *            name of the hook to set the output stream for
859 	 * @param hookStdOut
860 	 *            the output stream to use for the selected hook
861 	 * @return {@code this}
862 	 * @since 4.5
863 	 */
864 	public CommitCommand setHookOutputStream(String hookName,
865 			PrintStream hookStdOut) {
866 		if (!(PreCommitHook.NAME.equals(hookName)
867 				|| CommitMsgHook.NAME.equals(hookName)
868 				|| PostCommitHook.NAME.equals(hookName))) {
869 			throw new IllegalArgumentException(
870 					MessageFormat.format(JGitText.get().illegalHookName,
871 							hookName));
872 		}
873 		hookOutRedirect.put(hookName, hookStdOut);
874 		return this;
875 	}
876 }