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.LinkedList;
52  import java.util.List;
53  
54  import org.eclipse.jgit.api.errors.AbortedByHookException;
55  import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
56  import org.eclipse.jgit.api.errors.GitAPIException;
57  import org.eclipse.jgit.api.errors.JGitInternalException;
58  import org.eclipse.jgit.api.errors.NoFilepatternException;
59  import org.eclipse.jgit.api.errors.NoHeadException;
60  import org.eclipse.jgit.api.errors.NoMessageException;
61  import org.eclipse.jgit.api.errors.UnmergedPathsException;
62  import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
63  import org.eclipse.jgit.dircache.DirCache;
64  import org.eclipse.jgit.dircache.DirCacheBuildIterator;
65  import org.eclipse.jgit.dircache.DirCacheBuilder;
66  import org.eclipse.jgit.dircache.DirCacheEntry;
67  import org.eclipse.jgit.dircache.DirCacheIterator;
68  import org.eclipse.jgit.errors.UnmergedPathException;
69  import org.eclipse.jgit.hooks.Hooks;
70  import org.eclipse.jgit.internal.JGitText;
71  import org.eclipse.jgit.lib.CommitBuilder;
72  import org.eclipse.jgit.lib.Constants;
73  import org.eclipse.jgit.lib.FileMode;
74  import org.eclipse.jgit.lib.ObjectId;
75  import org.eclipse.jgit.lib.ObjectInserter;
76  import org.eclipse.jgit.lib.PersonIdent;
77  import org.eclipse.jgit.lib.Ref;
78  import org.eclipse.jgit.lib.RefUpdate;
79  import org.eclipse.jgit.lib.RefUpdate.Result;
80  import org.eclipse.jgit.lib.Repository;
81  import org.eclipse.jgit.lib.RepositoryState;
82  import org.eclipse.jgit.revwalk.RevCommit;
83  import org.eclipse.jgit.revwalk.RevObject;
84  import org.eclipse.jgit.revwalk.RevTag;
85  import org.eclipse.jgit.revwalk.RevWalk;
86  import org.eclipse.jgit.treewalk.CanonicalTreeParser;
87  import org.eclipse.jgit.treewalk.FileTreeIterator;
88  import org.eclipse.jgit.treewalk.TreeWalk;
89  import org.eclipse.jgit.util.ChangeIdUtil;
90  
91  /**
92   * A class used to execute a {@code Commit} command. It has setters for all
93   * supported options and arguments of this command and a {@link #call()} method
94   * to finally execute the command.
95   *
96   * @see <a
97   *      href="http://www.kernel.org/pub/software/scm/git/docs/git-commit.html"
98   *      >Git documentation about Commit</a>
99   */
100 public class CommitCommand extends GitCommand<RevCommit> {
101 	private PersonIdent author;
102 
103 	private PersonIdent committer;
104 
105 	private String message;
106 
107 	private boolean all;
108 
109 	private List<String> only = new ArrayList<String>();
110 
111 	private boolean[] onlyProcessed;
112 
113 	private boolean amend;
114 
115 	private boolean insertChangeId;
116 
117 	/**
118 	 * parents this commit should have. The current HEAD will be in this list
119 	 * and also all commits mentioned in .git/MERGE_HEAD
120 	 */
121 	private List<ObjectId> parents = new LinkedList<ObjectId>();
122 
123 	private String reflogComment;
124 
125 	/**
126 	 * Setting this option bypasses the pre-commit and commit-msg hooks.
127 	 */
128 	private boolean noVerify;
129 
130 	private PrintStream hookOutRedirect;
131 
132 	/**
133 	 * @param repo
134 	 */
135 	protected CommitCommand(Repository repo) {
136 		super(repo);
137 	}
138 
139 	/**
140 	 * Executes the {@code commit} command with all the options and parameters
141 	 * collected by the setter methods of this class. Each instance of this
142 	 * class should only be used for one invocation of the command (means: one
143 	 * call to {@link #call()})
144 	 *
145 	 * @return a {@link RevCommit} object representing the successful commit.
146 	 * @throws NoHeadException
147 	 *             when called on a git repo without a HEAD reference
148 	 * @throws NoMessageException
149 	 *             when called without specifying a commit message
150 	 * @throws UnmergedPathsException
151 	 *             when the current index contained unmerged paths (conflicts)
152 	 * @throws ConcurrentRefUpdateException
153 	 *             when HEAD or branch ref is updated concurrently by someone
154 	 *             else
155 	 * @throws WrongRepositoryStateException
156 	 *             when repository is not in the right state for committing
157 	 * @throws AbortedByHookException
158 	 *             if there are either pre-commit or commit-msg hooks present in
159 	 *             the repository and one of them rejects the commit.
160 	 */
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).call();
177 			}
178 
179 			processOptions(state, rw);
180 
181 			if (all && !repo.isBare() && repo.getWorkTree() != null) {
182 				try (Git git = new Git(repo)) {
183 					git.add()
184 							.addFilepattern(".") //$NON-NLS-1$
185 							.setUpdate(true).call();
186 				} catch (NoFilepatternException e) {
187 					// should really not happen
188 					throw new JGitInternalException(e.getMessage(), e);
189 				}
190 			}
191 
192 			Ref head = repo.getRef(Constants.HEAD);
193 			if (head == null)
194 				throw new NoHeadException(
195 						JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
196 
197 			// determine the current HEAD and the commit it is referring to
198 			ObjectId headId = repo.resolve(Constants.HEAD + "^{commit}"); //$NON-NLS-1$
199 			if (headId == null && amend)
200 				throw new WrongRepositoryStateException(
201 						JGitText.get().commitAmendOnInitialNotPossible);
202 
203 			if (headId != null)
204 				if (amend) {
205 					RevCommit previousCommit = rw.parseCommit(headId);
206 					for (RevCommit p : previousCommit.getParents())
207 						parents.add(p.getId());
208 					if (author == null)
209 						author = previousCommit.getAuthorIdent();
210 				} else {
211 					parents.add(0, headId);
212 				}
213 
214 			if (!noVerify) {
215 				message = Hooks.commitMsg(repo, hookOutRedirect)
216 						.setCommitMessage(message).call();
217 			}
218 
219 			// lock the index
220 			DirCache index = repo.lockDirCache();
221 			try (ObjectInserter odi = repo.newObjectInserter()) {
222 				if (!only.isEmpty())
223 					index = createTemporaryIndex(headId, index, rw);
224 
225 				// Write the index as tree to the object database. This may
226 				// fail for example when the index contains unmerged paths
227 				// (unresolved conflicts)
228 				ObjectId indexTreeId = index.writeTree(odi);
229 
230 				if (insertChangeId)
231 					insertChangeId(indexTreeId);
232 
233 				// Create a Commit object, populate it and write it
234 				CommitBuilder commit = new CommitBuilder();
235 				commit.setCommitter(committer);
236 				commit.setAuthor(author);
237 				commit.setMessage(message);
238 
239 				commit.setParentIds(parents);
240 				commit.setTreeId(indexTreeId);
241 				ObjectId commitId = odi.insert(commit);
242 				odi.flush();
243 
244 				RevCommit revCommit = rw.parseCommit(commitId);
245 				RefUpdate ru = repo.updateRef(Constants.HEAD);
246 				ru.setNewObjectId(commitId);
247 				if (reflogComment != null) {
248 					ru.setRefLogMessage(reflogComment, false);
249 				} else {
250 					String prefix = amend ? "commit (amend): " //$NON-NLS-1$
251 							: parents.size() == 0 ? "commit (initial): " //$NON-NLS-1$
252 									: "commit: "; //$NON-NLS-1$
253 					ru.setRefLogMessage(prefix + revCommit.getShortMessage(),
254 							false);
255 				}
256 				if (headId != null)
257 					ru.setExpectedOldObjectId(headId);
258 				else
259 					ru.setExpectedOldObjectId(ObjectId.zeroId());
260 				Result rc = ru.forceUpdate();
261 				switch (rc) {
262 				case NEW:
263 				case FORCED:
264 				case FAST_FORWARD: {
265 					setCallable(false);
266 					if (state == RepositoryState.MERGING_RESOLVED
267 							|| isMergeDuringRebase(state)) {
268 						// Commit was successful. Now delete the files
269 						// used for merge commits
270 						repo.writeMergeCommitMsg(null);
271 						repo.writeMergeHeads(null);
272 					} else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) {
273 						repo.writeMergeCommitMsg(null);
274 						repo.writeCherryPickHead(null);
275 					} else if (state == RepositoryState.REVERTING_RESOLVED) {
276 						repo.writeMergeCommitMsg(null);
277 						repo.writeRevertHead(null);
278 					}
279 					return revCommit;
280 				}
281 				case REJECTED:
282 				case LOCK_FAILURE:
283 					throw new ConcurrentRefUpdateException(
284 							JGitText.get().couldNotLockHEAD, ru.getRef(), rc);
285 				default:
286 					throw new JGitInternalException(MessageFormat.format(
287 							JGitText.get().updatingRefFailed, Constants.HEAD,
288 							commitId.toString(), rc));
289 				}
290 			} finally {
291 				index.unlock();
292 			}
293 		} catch (UnmergedPathException e) {
294 			throw new UnmergedPathsException(e);
295 		} catch (IOException e) {
296 			throw new JGitInternalException(
297 					JGitText.get().exceptionCaughtDuringExecutionOfCommitCommand, e);
298 		}
299 	}
300 
301 	private void insertChangeId(ObjectId treeId) throws IOException {
302 		ObjectId firstParentId = null;
303 		if (!parents.isEmpty())
304 			firstParentId = parents.get(0);
305 		ObjectId changeId = ChangeIdUtil.computeChangeId(treeId, firstParentId,
306 				author, committer, message);
307 		message = ChangeIdUtil.insertId(message, changeId);
308 		if (changeId != null)
309 			message = message.replaceAll("\nChange-Id: I" //$NON-NLS-1$
310 					+ ObjectId.zeroId().getName() + "\n", "\nChange-Id: I" //$NON-NLS-1$ //$NON-NLS-2$
311 					+ changeId.getName() + "\n"); //$NON-NLS-1$
312 	}
313 
314 	private DirCache createTemporaryIndex(ObjectId headId, DirCache index,
315 			RevWalk rw)
316 			throws IOException {
317 		ObjectInserter inserter = null;
318 
319 		// get DirCacheBuilder for existing index
320 		DirCacheBuilder existingBuilder = index.builder();
321 
322 		// get DirCacheBuilder for newly created in-core index to build a
323 		// temporary index for this commit
324 		DirCache inCoreIndex = DirCache.newInCore();
325 		DirCacheBuilder tempBuilder = inCoreIndex.builder();
326 
327 		onlyProcessed = new boolean[only.size()];
328 		boolean emptyCommit = true;
329 
330 		try (TreeWalk treeWalk = new TreeWalk(repo)) {
331 			int dcIdx = treeWalk
332 					.addTree(new DirCacheBuildIterator(existingBuilder));
333 			int fIdx = treeWalk.addTree(new FileTreeIterator(repo));
334 			int hIdx = -1;
335 			if (headId != null)
336 				hIdx = treeWalk.addTree(rw.parseTree(headId));
337 			treeWalk.setRecursive(true);
338 
339 			String lastAddedFile = null;
340 			while (treeWalk.next()) {
341 				String path = treeWalk.getPathString();
342 				// check if current entry's path matches a specified path
343 				int pos = lookupOnly(path);
344 
345 				CanonicalTreeParser hTree = null;
346 				if (hIdx != -1)
347 					hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
348 
349 				DirCacheIterator dcTree = treeWalk.getTree(dcIdx,
350 						DirCacheIterator.class);
351 
352 				if (pos >= 0) {
353 					// include entry in commit
354 
355 					FileTreeIterator fTree = treeWalk.getTree(fIdx,
356 							FileTreeIterator.class);
357 
358 					// check if entry refers to a tracked file
359 					boolean tracked = dcTree != null || hTree != null;
360 					if (!tracked)
361 						continue;
362 
363 					// for an unmerged path, DirCacheBuildIterator will yield 3
364 					// entries, we only want to add one
365 					if (path.equals(lastAddedFile))
366 						continue;
367 
368 					lastAddedFile = path;
369 
370 					if (fTree != null) {
371 						// create a new DirCacheEntry with data retrieved from
372 						// disk
373 						final DirCacheEntry dcEntry = new DirCacheEntry(path);
374 						long entryLength = fTree.getEntryLength();
375 						dcEntry.setLength(entryLength);
376 						dcEntry.setLastModified(fTree.getEntryLastModified());
377 						dcEntry.setFileMode(fTree.getIndexFileMode(dcTree));
378 
379 						boolean objectExists = (dcTree != null
380 								&& fTree.idEqual(dcTree))
381 								|| (hTree != null && fTree.idEqual(hTree));
382 						if (objectExists) {
383 							dcEntry.setObjectId(fTree.getEntryObjectId());
384 						} else {
385 							if (FileMode.GITLINK.equals(dcEntry.getFileMode()))
386 								dcEntry.setObjectId(fTree.getEntryObjectId());
387 							else {
388 								// insert object
389 								if (inserter == null)
390 									inserter = repo.newObjectInserter();
391 								long contentLength = fTree
392 										.getEntryContentLength();
393 								InputStream inputStream = fTree
394 										.openEntryStream();
395 								try {
396 									dcEntry.setObjectId(inserter.insert(
397 											Constants.OBJ_BLOB, contentLength,
398 											inputStream));
399 								} finally {
400 									inputStream.close();
401 								}
402 							}
403 						}
404 
405 						// add to existing index
406 						existingBuilder.add(dcEntry);
407 						// add to temporary in-core index
408 						tempBuilder.add(dcEntry);
409 
410 						if (emptyCommit
411 								&& (hTree == null || !hTree.idEqual(fTree)
412 										|| hTree.getEntryRawMode() != fTree
413 												.getEntryRawMode()))
414 							// this is a change
415 							emptyCommit = false;
416 					} else {
417 						// if no file exists on disk, neither add it to
418 						// index nor to temporary in-core index
419 
420 						if (emptyCommit && hTree != null)
421 							// this is a change
422 							emptyCommit = false;
423 					}
424 
425 					// keep track of processed path
426 					onlyProcessed[pos] = true;
427 				} else {
428 					// add entries from HEAD for all other paths
429 					if (hTree != null) {
430 						// create a new DirCacheEntry with data retrieved from
431 						// HEAD
432 						final DirCacheEntry dcEntry = new DirCacheEntry(path);
433 						dcEntry.setObjectId(hTree.getEntryObjectId());
434 						dcEntry.setFileMode(hTree.getEntryFileMode());
435 
436 						// add to temporary in-core index
437 						tempBuilder.add(dcEntry);
438 					}
439 
440 					// preserve existing entry in index
441 					if (dcTree != null)
442 						existingBuilder.add(dcTree.getDirCacheEntry());
443 				}
444 			}
445 		}
446 
447 		// there must be no unprocessed paths left at this point; otherwise an
448 		// untracked or unknown path has been specified
449 		for (int i = 0; i < onlyProcessed.length; i++)
450 			if (!onlyProcessed[i])
451 				throw new JGitInternalException(MessageFormat.format(
452 						JGitText.get().entryNotFoundByPath, only.get(i)));
453 
454 		// there must be at least one change
455 		if (emptyCommit)
456 			throw new JGitInternalException(JGitText.get().emptyCommit);
457 
458 		// update index
459 		existingBuilder.commit();
460 		// finish temporary in-core index used for this commit
461 		tempBuilder.finish();
462 		return inCoreIndex;
463 	}
464 
465 	/**
466 	 * Look an entry's path up in the list of paths specified by the --only/ -o
467 	 * option
468 	 *
469 	 * In case the complete (file) path (e.g. "d1/d2/f1") cannot be found in
470 	 * <code>only</code>, lookup is also tried with (parent) directory paths
471 	 * (e.g. "d1/d2" and "d1").
472 	 *
473 	 * @param pathString
474 	 *            entry's path
475 	 * @return the item's index in <code>only</code>; -1 if no item matches
476 	 */
477 	private int lookupOnly(String pathString) {
478 		String p = pathString;
479 		while (true) {
480 			int position = Collections.binarySearch(only, p);
481 			if (position >= 0)
482 				return position;
483 			int l = p.lastIndexOf("/"); //$NON-NLS-1$
484 			if (l < 1)
485 				break;
486 			p = p.substring(0, l);
487 		}
488 		return -1;
489 	}
490 
491 	/**
492 	 * Sets default values for not explicitly specified options. Then validates
493 	 * that all required data has been provided.
494 	 *
495 	 * @param state
496 	 *            the state of the repository we are working on
497 	 * @param rw
498 	 *            the RevWalk to use
499 	 *
500 	 * @throws NoMessageException
501 	 *             if the commit message has not been specified
502 	 */
503 	private void processOptions(RepositoryState state, RevWalk rw)
504 			throws NoMessageException {
505 		if (committer == null)
506 			committer = new PersonIdent(repo);
507 		if (author == null && !amend)
508 			author = committer;
509 
510 		// when doing a merge commit parse MERGE_HEAD and MERGE_MSG files
511 		if (state == RepositoryState.MERGING_RESOLVED
512 				|| isMergeDuringRebase(state)) {
513 			try {
514 				parents = repo.readMergeHeads();
515 				if (parents != null)
516 					for (int i = 0; i < parents.size(); i++) {
517 						RevObject ro = rw.parseAny(parents.get(i));
518 						if (ro instanceof RevTag)
519 							parents.set(i, rw.peel(ro));
520 					}
521 			} catch (IOException e) {
522 				throw new JGitInternalException(MessageFormat.format(
523 						JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
524 						Constants.MERGE_HEAD, e), e);
525 			}
526 			if (message == null) {
527 				try {
528 					message = repo.readMergeCommitMsg();
529 				} catch (IOException e) {
530 					throw new JGitInternalException(MessageFormat.format(
531 							JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
532 							Constants.MERGE_MSG, e), e);
533 				}
534 			}
535 		} else if (state == RepositoryState.SAFE && message == null) {
536 			try {
537 				message = repo.readSquashCommitMsg();
538 				if (message != null)
539 					repo.writeSquashCommitMsg(null /* delete */);
540 			} catch (IOException e) {
541 				throw new JGitInternalException(MessageFormat.format(
542 						JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
543 						Constants.MERGE_MSG, e), e);
544 			}
545 
546 		}
547 		if (message == null)
548 			// as long as we don't support -C option we have to have
549 			// an explicit message
550 			throw new NoMessageException(JGitText.get().commitMessageNotSpecified);
551 	}
552 
553 	private boolean isMergeDuringRebase(RepositoryState state) {
554 		if (state != RepositoryState.REBASING_INTERACTIVE
555 				&& state != RepositoryState.REBASING_MERGE)
556 			return false;
557 		try {
558 			return repo.readMergeHeads() != null;
559 		} catch (IOException e) {
560 			throw new JGitInternalException(MessageFormat.format(
561 					JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR,
562 					Constants.MERGE_HEAD, e), e);
563 		}
564 	}
565 
566 	/**
567 	 * @param message
568 	 *            the commit message used for the {@code commit}
569 	 * @return {@code this}
570 	 */
571 	public CommitCommand setMessage(String message) {
572 		checkCallable();
573 		this.message = message;
574 		return this;
575 	}
576 
577 	/**
578 	 * @return the commit message used for the <code>commit</code>
579 	 */
580 	public String getMessage() {
581 		return message;
582 	}
583 
584 	/**
585 	 * Sets the committer for this {@code commit}. If no committer is explicitly
586 	 * specified because this method is never called or called with {@code null}
587 	 * value then the committer will be deduced from config info in repository,
588 	 * with current time.
589 	 *
590 	 * @param committer
591 	 *            the committer used for the {@code commit}
592 	 * @return {@code this}
593 	 */
594 	public CommitCommand setCommitter(PersonIdent committer) {
595 		checkCallable();
596 		this.committer = committer;
597 		return this;
598 	}
599 
600 	/**
601 	 * Sets the committer for this {@code commit}. If no committer is explicitly
602 	 * specified because this method is never called then the committer will be
603 	 * deduced from config info in repository, with current time.
604 	 *
605 	 * @param name
606 	 *            the name of the committer used for the {@code commit}
607 	 * @param email
608 	 *            the email of the committer used for the {@code commit}
609 	 * @return {@code this}
610 	 */
611 	public CommitCommand setCommitter(String name, String email) {
612 		checkCallable();
613 		return setCommitter(new PersonIdent(name, email));
614 	}
615 
616 	/**
617 	 * @return the committer used for the {@code commit}. If no committer was
618 	 *         specified {@code null} is returned and the default
619 	 *         {@link PersonIdent} of this repo is used during execution of the
620 	 *         command
621 	 */
622 	public PersonIdent getCommitter() {
623 		return committer;
624 	}
625 
626 	/**
627 	 * Sets the author for this {@code commit}. If no author is explicitly
628 	 * specified because this method is never called or called with {@code null}
629 	 * value then the author will be set to the committer or to the original
630 	 * author when amending.
631 	 *
632 	 * @param author
633 	 *            the author used for the {@code commit}
634 	 * @return {@code this}
635 	 */
636 	public CommitCommand setAuthor(PersonIdent author) {
637 		checkCallable();
638 		this.author = author;
639 		return this;
640 	}
641 
642 	/**
643 	 * Sets the author for this {@code commit}. If no author is explicitly
644 	 * specified because this method is never called then the author will be set
645 	 * to the committer or to the original author when amending.
646 	 *
647 	 * @param name
648 	 *            the name of the author used for the {@code commit}
649 	 * @param email
650 	 *            the email of the author used for the {@code commit}
651 	 * @return {@code this}
652 	 */
653 	public CommitCommand setAuthor(String name, String email) {
654 		checkCallable();
655 		return setAuthor(new PersonIdent(name, email));
656 	}
657 
658 	/**
659 	 * @return the author used for the {@code commit}. If no author was
660 	 *         specified {@code null} is returned and the default
661 	 *         {@link PersonIdent} of this repo is used during execution of the
662 	 *         command
663 	 */
664 	public PersonIdent getAuthor() {
665 		return author;
666 	}
667 
668 	/**
669 	 * If set to true the Commit command automatically stages files that have
670 	 * been modified and deleted, but new files not known by the repository are
671 	 * not affected. This corresponds to the parameter -a on the command line.
672 	 *
673 	 * @param all
674 	 * @return {@code this}
675 	 * @throws JGitInternalException
676 	 *             in case of an illegal combination of arguments/ options
677 	 */
678 	public CommitCommand setAll(boolean all) {
679 		checkCallable();
680 		if (!only.isEmpty())
681 			throw new JGitInternalException(MessageFormat.format(
682 					JGitText.get().illegalCombinationOfArguments, "--all", //$NON-NLS-1$
683 					"--only")); //$NON-NLS-1$
684 		this.all = all;
685 		return this;
686 	}
687 
688 	/**
689 	 * Used to amend the tip of the current branch. If set to true, the previous
690 	 * commit will be amended. This is equivalent to --amend on the command
691 	 * line.
692 	 *
693 	 * @param amend
694 	 * @return {@code this}
695 	 */
696 	public CommitCommand setAmend(boolean amend) {
697 		checkCallable();
698 		this.amend = amend;
699 		return this;
700 	}
701 
702 	/**
703 	 * Commit dedicated path only.
704 	 * <p>
705 	 * This method can be called several times to add multiple paths. Full file
706 	 * paths are supported as well as directory paths; in the latter case this
707 	 * commits all files/directories below the specified path.
708 	 *
709 	 * @param only
710 	 *            path to commit (with <code>/</code> as separator)
711 	 * @return {@code this}
712 	 */
713 	public CommitCommand setOnly(String only) {
714 		checkCallable();
715 		if (all)
716 			throw new JGitInternalException(MessageFormat.format(
717 					JGitText.get().illegalCombinationOfArguments, "--only", //$NON-NLS-1$
718 					"--all")); //$NON-NLS-1$
719 		String o = only.endsWith("/") ? only.substring(0, only.length() - 1) //$NON-NLS-1$
720 				: only;
721 		// ignore duplicates
722 		if (!this.only.contains(o))
723 			this.only.add(o);
724 		return this;
725 	}
726 
727 	/**
728 	 * If set to true a change id will be inserted into the commit message
729 	 *
730 	 * An existing change id is not replaced. An initial change id (I000...)
731 	 * will be replaced by the change id.
732 	 *
733 	 * @param insertChangeId
734 	 *
735 	 * @return {@code this}
736 	 */
737 	public CommitCommand setInsertChangeId(boolean insertChangeId) {
738 		checkCallable();
739 		this.insertChangeId = insertChangeId;
740 		return this;
741 	}
742 
743 	/**
744 	 * Override the message written to the reflog
745 	 *
746 	 * @param reflogComment
747 	 * @return {@code this}
748 	 */
749 	public CommitCommand setReflogComment(String reflogComment) {
750 		this.reflogComment = reflogComment;
751 		return this;
752 	}
753 
754 	/**
755 	 * Sets the {@link #noVerify} option on this commit command.
756 	 * <p>
757 	 * Both the pre-commit and commit-msg hooks can block a commit by their
758 	 * return value; setting this option to <code>true</code> will bypass these
759 	 * two hooks.
760 	 * </p>
761 	 *
762 	 * @param noVerify
763 	 *            Whether this commit should be verified by the pre-commit and
764 	 *            commit-msg hooks.
765 	 * @return {@code this}
766 	 * @since 3.7
767 	 */
768 	public CommitCommand setNoVerify(boolean noVerify) {
769 		this.noVerify = noVerify;
770 		return this;
771 	}
772 
773 	/**
774 	 * Set the output stream for hook scripts executed by this command. If not
775 	 * set it defaults to {@code System.out}.
776 	 *
777 	 * @param hookStdOut
778 	 *            the output stream for hook scripts executed by this command
779 	 * @return {@code this}
780 	 * @since 3.7
781 	 */
782 	public CommitCommand setHookOutputStream(PrintStream hookStdOut) {
783 		this.hookOutRedirect = hookStdOut;
784 		return this;
785 	}
786 }