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.EmtpyCommitException;
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 	 * @param repo
144 	 */
145 	protected CommitCommand(Repository repo) {
146 		super(repo);
147 	}
148 
149 	/**
150 	 * Executes the {@code commit} command with all the options and parameters
151 	 * collected by the setter methods of this class. Each instance of this
152 	 * class should only be used for one invocation of the command (means: one
153 	 * call to {@link #call()})
154 	 *
155 	 * @return a {@link RevCommit} object representing the successful commit.
156 	 * @throws NoHeadException
157 	 *             when called on a git repo without a HEAD reference
158 	 * @throws NoMessageException
159 	 *             when called without specifying a commit message
160 	 * @throws UnmergedPathsException
161 	 *             when the current index contained unmerged paths (conflicts)
162 	 * @throws ConcurrentRefUpdateException
163 	 *             when HEAD or branch ref is updated concurrently by someone
164 	 *             else
165 	 * @throws WrongRepositoryStateException
166 	 *             when repository is not in the right state for committing
167 	 * @throws AbortedByHookException
168 	 *             if there are either pre-commit or commit-msg hooks present in
169 	 *             the repository and one of them rejects the commit.
170 	 */
171 	@Override
172 	public RevCommit call() throws GitAPIException, NoHeadException,
173 			NoMessageException, UnmergedPathsException,
174 			ConcurrentRefUpdateException, WrongRepositoryStateException,
175 			AbortedByHookException {
176 		checkCallable();
177 		Collections.sort(only);
178 
179 		try (RevWalk rw = new RevWalk(repo)) {
180 			RepositoryState state = repo.getRepositoryState();
181 			if (!state.canCommit())
182 				throw new WrongRepositoryStateException(MessageFormat.format(
183 						JGitText.get().cannotCommitOnARepoWithState,
184 						state.name()));
185 
186 			if (!noVerify) {
187 				Hooks.preCommit(repo, hookOutRedirect.get(PreCommitHook.NAME))
188 						.call();
189 			}
190 
191 			processOptions(state, rw);
192 
193 			if (all && !repo.isBare()) {
194 				try (Git git = new Git(repo)) {
195 					git.add()
196 							.addFilepattern(".") //$NON-NLS-1$
197 							.setUpdate(true).call();
198 				} catch (NoFilepatternException e) {
199 					// should really not happen
200 					throw new JGitInternalException(e.getMessage(), e);
201 				}
202 			}
203 
204 			Ref head = repo.exactRef(Constants.HEAD);
205 			if (head == null)
206 				throw new NoHeadException(
207 						JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
208 
209 			// determine the current HEAD and the commit it is referring to
210 			ObjectId headId = repo.resolve(Constants.HEAD + "^{commit}"); //$NON-NLS-1$
211 			if (headId == null && amend)
212 				throw new WrongRepositoryStateException(
213 						JGitText.get().commitAmendOnInitialNotPossible);
214 
215 			if (headId != null)
216 				if (amend) {
217 					RevCommit previousCommit = rw.parseCommit(headId);
218 					for (RevCommit p : previousCommit.getParents())
219 						parents.add(p.getId());
220 					if (author == null)
221 						author = previousCommit.getAuthorIdent();
222 				} else {
223 					parents.add(0, headId);
224 				}
225 
226 			if (!noVerify) {
227 				message = Hooks
228 						.commitMsg(repo,
229 								hookOutRedirect.get(CommitMsgHook.NAME))
230 						.setCommitMessage(message).call();
231 			}
232 
233 			// lock the index
234 			DirCache index = repo.lockDirCache();
235 			try (ObjectInserter odi = repo.newObjectInserter()) {
236 				if (!only.isEmpty())
237 					index = createTemporaryIndex(headId, index, rw);
238 
239 				// Write the index as tree to the object database. This may
240 				// fail for example when the index contains unmerged paths
241 				// (unresolved conflicts)
242 				ObjectId indexTreeId = index.writeTree(odi);
243 
244 				if (insertChangeId)
245 					insertChangeId(indexTreeId);
246 
247 				// Check for empty commits
248 				if (headId != null && !allowEmpty.booleanValue()) {
249 					RevCommit headCommit = rw.parseCommit(headId);
250 					headCommit.getTree();
251 					if (indexTreeId.equals(headCommit.getTree())) {
252 						throw new EmtpyCommitException(
253 								JGitText.get().emptyCommit);
254 					}
255 				}
256 
257 				// Create a Commit object, populate it and write it
258 				CommitBuilder commit = new CommitBuilder();
259 				commit.setCommitter(committer);
260 				commit.setAuthor(author);
261 				commit.setMessage(message);
262 
263 				commit.setParentIds(parents);
264 				commit.setTreeId(indexTreeId);
265 				ObjectId commitId = odi.insert(commit);
266 				odi.flush();
267 
268 				RevCommit revCommit = rw.parseCommit(commitId);
269 				RefUpdate ru = repo.updateRef(Constants.HEAD);
270 				ru.setNewObjectId(commitId);
271 				if (!useDefaultReflogMessage) {
272 					ru.setRefLogMessage(reflogComment, false);
273 				} else {
274 					String prefix = amend ? "commit (amend): " //$NON-NLS-1$
275 							: parents.size() == 0 ? "commit (initial): " //$NON-NLS-1$
276 									: "commit: "; //$NON-NLS-1$
277 					ru.setRefLogMessage(prefix + revCommit.getShortMessage(),
278 							false);
279 				}
280 				if (headId != null)
281 					ru.setExpectedOldObjectId(headId);
282 				else
283 					ru.setExpectedOldObjectId(ObjectId.zeroId());
284 				Result rc = ru.forceUpdate();
285 				switch (rc) {
286 				case NEW:
287 				case FORCED:
288 				case FAST_FORWARD: {
289 					setCallable(false);
290 					if (state == RepositoryState.MERGING_RESOLVED
291 							|| isMergeDuringRebase(state)) {
292 						// Commit was successful. Now delete the files
293 						// used for merge commits
294 						repo.writeMergeCommitMsg(null);
295 						repo.writeMergeHeads(null);
296 					} else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) {
297 						repo.writeMergeCommitMsg(null);
298 						repo.writeCherryPickHead(null);
299 					} else if (state == RepositoryState.REVERTING_RESOLVED) {
300 						repo.writeMergeCommitMsg(null);
301 						repo.writeRevertHead(null);
302 					}
303 					Hooks.postCommit(repo,
304 							hookOutRedirect.get(PostCommitHook.NAME)).call();
305 
306 					return revCommit;
307 				}
308 				case REJECTED:
309 				case LOCK_FAILURE:
310 					throw new ConcurrentRefUpdateException(
311 							JGitText.get().couldNotLockHEAD, ru.getRef(), rc);
312 				default:
313 					throw new JGitInternalException(MessageFormat.format(
314 							JGitText.get().updatingRefFailed, Constants.HEAD,
315 							commitId.toString(), rc));
316 				}
317 			} finally {
318 				index.unlock();
319 			}
320 		} catch (UnmergedPathException e) {
321 			throw new UnmergedPathsException(e);
322 		} catch (IOException e) {
323 			throw new JGitInternalException(
324 					JGitText.get().exceptionCaughtDuringExecutionOfCommitCommand, e);
325 		}
326 	}
327 
328 	private void insertChangeId(ObjectId treeId) {
329 		ObjectId firstParentId = null;
330 		if (!parents.isEmpty())
331 			firstParentId = parents.get(0);
332 		ObjectId changeId = ChangeIdUtil.computeChangeId(treeId, firstParentId,
333 				author, committer, message);
334 		message = ChangeIdUtil.insertId(message, changeId);
335 		if (changeId != null)
336 			message = message.replaceAll("\nChange-Id: I" //$NON-NLS-1$
337 					+ ObjectId.zeroId().getName() + "\n", "\nChange-Id: I" //$NON-NLS-1$ //$NON-NLS-2$
338 					+ changeId.getName() + "\n"); //$NON-NLS-1$
339 	}
340 
341 	private DirCache createTemporaryIndex(ObjectId headId, DirCache index,
342 			RevWalk rw)
343 			throws IOException {
344 		ObjectInserter inserter = null;
345 
346 		// get DirCacheBuilder for existing index
347 		DirCacheBuilder existingBuilder = index.builder();
348 
349 		// get DirCacheBuilder for newly created in-core index to build a
350 		// temporary index for this commit
351 		DirCache inCoreIndex = DirCache.newInCore();
352 		DirCacheBuilder tempBuilder = inCoreIndex.builder();
353 
354 		onlyProcessed = new boolean[only.size()];
355 		boolean emptyCommit = true;
356 
357 		try (TreeWalk treeWalk = new TreeWalk(repo)) {
358 			treeWalk.setOperationType(OperationType.CHECKIN_OP);
359 			int dcIdx = treeWalk
360 					.addTree(new DirCacheBuildIterator(existingBuilder));
361 			FileTreeIterator fti = new FileTreeIterator(repo);
362 			fti.setDirCacheIterator(treeWalk, 0);
363 			int fIdx = treeWalk.addTree(fti);
364 			int hIdx = -1;
365 			if (headId != null)
366 				hIdx = treeWalk.addTree(rw.parseTree(headId));
367 			treeWalk.setRecursive(true);
368 
369 			String lastAddedFile = null;
370 			while (treeWalk.next()) {
371 				String path = treeWalk.getPathString();
372 				// check if current entry's path matches a specified path
373 				int pos = lookupOnly(path);
374 
375 				CanonicalTreeParser hTree = null;
376 				if (hIdx != -1)
377 					hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
378 
379 				DirCacheIterator dcTree = treeWalk.getTree(dcIdx,
380 						DirCacheIterator.class);
381 
382 				if (pos >= 0) {
383 					// include entry in commit
384 
385 					FileTreeIterator fTree = treeWalk.getTree(fIdx,
386 							FileTreeIterator.class);
387 
388 					// check if entry refers to a tracked file
389 					boolean tracked = dcTree != null || hTree != null;
390 					if (!tracked)
391 						continue;
392 
393 					// for an unmerged path, DirCacheBuildIterator will yield 3
394 					// entries, we only want to add one
395 					if (path.equals(lastAddedFile))
396 						continue;
397 
398 					lastAddedFile = path;
399 
400 					if (fTree != null) {
401 						// create a new DirCacheEntry with data retrieved from
402 						// disk
403 						final DirCacheEntry dcEntry = new DirCacheEntry(path);
404 						long entryLength = fTree.getEntryLength();
405 						dcEntry.setLength(entryLength);
406 						dcEntry.setLastModified(fTree.getEntryLastModified());
407 						dcEntry.setFileMode(fTree.getIndexFileMode(dcTree));
408 
409 						boolean objectExists = (dcTree != null
410 								&& fTree.idEqual(dcTree))
411 								|| (hTree != null && fTree.idEqual(hTree));
412 						if (objectExists) {
413 							dcEntry.setObjectId(fTree.getEntryObjectId());
414 						} else {
415 							if (FileMode.GITLINK.equals(dcEntry.getFileMode()))
416 								dcEntry.setObjectId(fTree.getEntryObjectId());
417 							else {
418 								// insert object
419 								if (inserter == null)
420 									inserter = repo.newObjectInserter();
421 								long contentLength = fTree
422 										.getEntryContentLength();
423 								InputStream inputStream = fTree
424 										.openEntryStream();
425 								try {
426 									dcEntry.setObjectId(inserter.insert(
427 											Constants.OBJ_BLOB, contentLength,
428 											inputStream));
429 								} finally {
430 									inputStream.close();
431 								}
432 							}
433 						}
434 
435 						// add to existing index
436 						existingBuilder.add(dcEntry);
437 						// add to temporary in-core index
438 						tempBuilder.add(dcEntry);
439 
440 						if (emptyCommit
441 								&& (hTree == null || !hTree.idEqual(fTree)
442 										|| hTree.getEntryRawMode() != fTree
443 												.getEntryRawMode()))
444 							// this is a change
445 							emptyCommit = false;
446 					} else {
447 						// if no file exists on disk, neither add it to
448 						// index nor to temporary in-core index
449 
450 						if (emptyCommit && hTree != null)
451 							// this is a change
452 							emptyCommit = false;
453 					}
454 
455 					// keep track of processed path
456 					onlyProcessed[pos] = true;
457 				} else {
458 					// add entries from HEAD for all other paths
459 					if (hTree != null) {
460 						// create a new DirCacheEntry with data retrieved from
461 						// HEAD
462 						final DirCacheEntry dcEntry = new DirCacheEntry(path);
463 						dcEntry.setObjectId(hTree.getEntryObjectId());
464 						dcEntry.setFileMode(hTree.getEntryFileMode());
465 
466 						// add to temporary in-core index
467 						tempBuilder.add(dcEntry);
468 					}
469 
470 					// preserve existing entry in index
471 					if (dcTree != null)
472 						existingBuilder.add(dcTree.getDirCacheEntry());
473 				}
474 			}
475 		}
476 
477 		// there must be no unprocessed paths left at this point; otherwise an
478 		// untracked or unknown path has been specified
479 		for (int i = 0; i < onlyProcessed.length; i++)
480 			if (!onlyProcessed[i])
481 				throw new JGitInternalException(MessageFormat.format(
482 						JGitText.get().entryNotFoundByPath, only.get(i)));
483 
484 		// there must be at least one change
485 		if (emptyCommit)
486 			// Would like to throw a EmptyCommitException. But this would break the API
487 			// TODO(ch): Change this in the next release
488 			throw new JGitInternalException(JGitText.get().emptyCommit);
489 
490 		// update index
491 		existingBuilder.commit();
492 		// finish temporary in-core index used for this commit
493 		tempBuilder.finish();
494 		return inCoreIndex;
495 	}
496 
497 	/**
498 	 * Look an entry's path up in the list of paths specified by the --only/ -o
499 	 * option
500 	 *
501 	 * In case the complete (file) path (e.g. "d1/d2/f1") cannot be found in
502 	 * <code>only</code>, lookup is also tried with (parent) directory paths
503 	 * (e.g. "d1/d2" and "d1").
504 	 *
505 	 * @param pathString
506 	 *            entry's path
507 	 * @return the item's index in <code>only</code>; -1 if no item matches
508 	 */
509 	private int lookupOnly(String pathString) {
510 		String p = pathString;
511 		while (true) {
512 			int position = Collections.binarySearch(only, p);
513 			if (position >= 0)
514 				return position;
515 			int l = p.lastIndexOf("/"); //$NON-NLS-1$
516 			if (l < 1)
517 				break;
518 			p = p.substring(0, l);
519 		}
520 		return -1;
521 	}
522 
523 	/**
524 	 * Sets default values for not explicitly specified options. Then validates
525 	 * that all required data has been provided.
526 	 *
527 	 * @param state
528 	 *            the state of the repository we are working on
529 	 * @param rw
530 	 *            the RevWalk to use
531 	 *
532 	 * @throws NoMessageException
533 	 *             if the commit message has not been specified
534 	 */
535 	private void processOptions(RepositoryState state, RevWalk rw)
536 			throws NoMessageException {
537 		if (committer == null)
538 			committer = new PersonIdent(repo);
539 		if (author == null && !amend)
540 			author = committer;
541 		if (allowEmpty == null)
542 			// JGit allows empty commits by default. Only when pathes are
543 			// specified the commit should not be empty. This behaviour differs
544 			// from native git but can only be adapted in the next release.
545 			// TODO(ch) align the defaults with native git
546 			allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE;
547 
548 		// when doing a merge commit parse MERGE_HEAD and MERGE_MSG files
549 		if (state == RepositoryState.MERGING_RESOLVED
550 				|| isMergeDuringRebase(state)) {
551 			try {
552 				parents = repo.readMergeHeads();
553 				if (parents != null)
554 					for (int i = 0; i < parents.size(); i++) {
555 						RevObject ro = rw.parseAny(parents.get(i));
556 						if (ro instanceof RevTag)
557 							parents.set(i, rw.peel(ro));
558 					}
559 			} catch (IOException e) {
560 				throw new JGitInternalException(MessageFormat.format(
561 						JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
562 						Constants.MERGE_HEAD, e), e);
563 			}
564 			if (message == null) {
565 				try {
566 					message = repo.readMergeCommitMsg();
567 				} catch (IOException e) {
568 					throw new JGitInternalException(MessageFormat.format(
569 							JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
570 							Constants.MERGE_MSG, e), e);
571 				}
572 			}
573 		} else if (state == RepositoryState.SAFE && message == null) {
574 			try {
575 				message = repo.readSquashCommitMsg();
576 				if (message != null)
577 					repo.writeSquashCommitMsg(null /* delete */);
578 			} catch (IOException e) {
579 				throw new JGitInternalException(MessageFormat.format(
580 						JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
581 						Constants.MERGE_MSG, e), e);
582 			}
583 
584 		}
585 		if (message == null)
586 			// as long as we don't support -C option we have to have
587 			// an explicit message
588 			throw new NoMessageException(JGitText.get().commitMessageNotSpecified);
589 	}
590 
591 	private boolean isMergeDuringRebase(RepositoryState state) {
592 		if (state != RepositoryState.REBASING_INTERACTIVE
593 				&& state != RepositoryState.REBASING_MERGE)
594 			return false;
595 		try {
596 			return repo.readMergeHeads() != null;
597 		} catch (IOException e) {
598 			throw new JGitInternalException(MessageFormat.format(
599 					JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
600 					Constants.MERGE_HEAD, e), e);
601 		}
602 	}
603 
604 	/**
605 	 * @param message
606 	 *            the commit message used for the {@code commit}
607 	 * @return {@code this}
608 	 */
609 	public CommitCommand setMessage(String message) {
610 		checkCallable();
611 		this.message = message;
612 		return this;
613 	}
614 
615 	/**
616 	 * @param allowEmpty
617 	 *            whether it should be allowed to create a commit which has the
618 	 *            same tree as it's sole predecessor (a commit which doesn't
619 	 *            change anything). By default when creating standard commits
620 	 *            (without specifying paths) JGit allows to create such commits.
621 	 *            When this flag is set to false an attempt to create an "empty"
622 	 *            standard commit will lead to an EmptyCommitException.
623 	 *            <p>
624 	 *            By default when creating a commit containing only specified
625 	 *            paths an attempt to create an empty commit leads to a
626 	 *            {@link JGitInternalException}. By setting this flag to
627 	 *            <code>true</code> this exception will not be thrown.
628 	 * @return {@code this}
629 	 * @since 4.2
630 	 */
631 	public CommitCommand setAllowEmpty(boolean allowEmpty) {
632 		this.allowEmpty = Boolean.valueOf(allowEmpty);
633 		return this;
634 	}
635 
636 	/**
637 	 * @return the commit message used for the <code>commit</code>
638 	 */
639 	public String getMessage() {
640 		return message;
641 	}
642 
643 	/**
644 	 * Sets the committer for this {@code commit}. If no committer is explicitly
645 	 * specified because this method is never called or called with {@code null}
646 	 * value then the committer will be deduced from config info in repository,
647 	 * with current time.
648 	 *
649 	 * @param committer
650 	 *            the committer used for the {@code commit}
651 	 * @return {@code this}
652 	 */
653 	public CommitCommand setCommitter(PersonIdent committer) {
654 		checkCallable();
655 		this.committer = committer;
656 		return this;
657 	}
658 
659 	/**
660 	 * Sets the committer for this {@code commit}. If no committer is explicitly
661 	 * specified because this method is never called then the committer will be
662 	 * deduced from config info in repository, with current time.
663 	 *
664 	 * @param name
665 	 *            the name of the committer used for the {@code commit}
666 	 * @param email
667 	 *            the email of the committer used for the {@code commit}
668 	 * @return {@code this}
669 	 */
670 	public CommitCommand setCommitter(String name, String email) {
671 		checkCallable();
672 		return setCommitter(new PersonIdent(name, email));
673 	}
674 
675 	/**
676 	 * @return the committer used for the {@code commit}. If no committer was
677 	 *         specified {@code null} is returned and the default
678 	 *         {@link PersonIdent} of this repo is used during execution of the
679 	 *         command
680 	 */
681 	public PersonIdent getCommitter() {
682 		return committer;
683 	}
684 
685 	/**
686 	 * Sets the author for this {@code commit}. If no author is explicitly
687 	 * specified because this method is never called or called with {@code null}
688 	 * value then the author will be set to the committer or to the original
689 	 * author when amending.
690 	 *
691 	 * @param author
692 	 *            the author used for the {@code commit}
693 	 * @return {@code this}
694 	 */
695 	public CommitCommand setAuthor(PersonIdent author) {
696 		checkCallable();
697 		this.author = author;
698 		return this;
699 	}
700 
701 	/**
702 	 * Sets the author for this {@code commit}. If no author is explicitly
703 	 * specified because this method is never called then the author will be set
704 	 * to the committer or to the original author when amending.
705 	 *
706 	 * @param name
707 	 *            the name of the author used for the {@code commit}
708 	 * @param email
709 	 *            the email of the author used for the {@code commit}
710 	 * @return {@code this}
711 	 */
712 	public CommitCommand setAuthor(String name, String email) {
713 		checkCallable();
714 		return setAuthor(new PersonIdent(name, email));
715 	}
716 
717 	/**
718 	 * @return the author used for the {@code commit}. If no author was
719 	 *         specified {@code null} is returned and the default
720 	 *         {@link PersonIdent} of this repo is used during execution of the
721 	 *         command
722 	 */
723 	public PersonIdent getAuthor() {
724 		return author;
725 	}
726 
727 	/**
728 	 * If set to true the Commit command automatically stages files that have
729 	 * been modified and deleted, but new files not known by the repository are
730 	 * not affected. This corresponds to the parameter -a on the command line.
731 	 *
732 	 * @param all
733 	 * @return {@code this}
734 	 * @throws JGitInternalException
735 	 *             in case of an illegal combination of arguments/ options
736 	 */
737 	public CommitCommand setAll(boolean all) {
738 		checkCallable();
739 		if (all && !only.isEmpty())
740 			throw new JGitInternalException(MessageFormat.format(
741 					JGitText.get().illegalCombinationOfArguments, "--all", //$NON-NLS-1$
742 					"--only")); //$NON-NLS-1$
743 		this.all = all;
744 		return this;
745 	}
746 
747 	/**
748 	 * Used to amend the tip of the current branch. If set to true, the previous
749 	 * commit will be amended. This is equivalent to --amend on the command
750 	 * line.
751 	 *
752 	 * @param amend
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 	 *
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 }