View Javadoc
1   /*
2    * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.api;
11  
12  import java.io.IOException;
13  import java.text.MessageFormat;
14  import java.util.ArrayList;
15  import java.util.List;
16  
17  import org.eclipse.jgit.api.errors.GitAPIException;
18  import org.eclipse.jgit.api.errors.JGitInternalException;
19  import org.eclipse.jgit.api.errors.NoHeadException;
20  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
21  import org.eclipse.jgit.errors.MissingObjectException;
22  import org.eclipse.jgit.internal.JGitText;
23  import org.eclipse.jgit.lib.AnyObjectId;
24  import org.eclipse.jgit.lib.Constants;
25  import org.eclipse.jgit.lib.ObjectId;
26  import org.eclipse.jgit.lib.Ref;
27  import org.eclipse.jgit.lib.Repository;
28  import org.eclipse.jgit.revwalk.RevCommit;
29  import org.eclipse.jgit.revwalk.RevWalk;
30  import org.eclipse.jgit.revwalk.filter.AndRevFilter;
31  import org.eclipse.jgit.revwalk.filter.MaxCountRevFilter;
32  import org.eclipse.jgit.revwalk.filter.RevFilter;
33  import org.eclipse.jgit.revwalk.filter.SkipRevFilter;
34  import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
35  import org.eclipse.jgit.treewalk.filter.PathFilter;
36  import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
37  import org.eclipse.jgit.treewalk.filter.TreeFilter;
38  
39  /**
40   * A class used to execute a {@code Log} command. It has setters for all
41   * supported options and arguments of this command and a {@link #call()} method
42   * to finally execute the command. Each instance of this class should only be
43   * used for one invocation of the command (means: one call to {@link #call()})
44   * <p>
45   * Examples (<code>git</code> is a {@link org.eclipse.jgit.api.Git} instance):
46   * <p>
47   * Get newest 10 commits, starting from the current branch:
48   *
49   * <pre>
50   * ObjectId head = repository.resolve(Constants.HEAD);
51   *
52   * Iterable&lt;RevCommit&gt; commits = git.log().add(head).setMaxCount(10).call();
53   * </pre>
54   * <p>
55   *
56   * <p>
57   * Get commits only for a specific file:
58   *
59   * <pre>
60   * git.log().add(head).addPath(&quot;dir/filename.txt&quot;).call();
61   * </pre>
62   * <p>
63   *
64   * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-log.html"
65   *      >Git documentation about Log</a>
66   */
67  public class LogCommand extends GitCommand<Iterable<RevCommit>> {
68  	private RevWalk walk;
69  
70  	private boolean startSpecified = false;
71  
72  	private RevFilter revFilter;
73  
74  	private final List<PathFilter> pathFilters = new ArrayList<>();
75  	private final List<TreeFilter> excludeTreeFilters = new ArrayList<>();
76  
77  	private int maxCount = -1;
78  
79  	private int skip = -1;
80  
81  	/**
82  	 * Constructor for LogCommand.
83  	 *
84  	 * @param repo
85  	 *            the {@link org.eclipse.jgit.lib.Repository}
86  	 */
87  	protected LogCommand(Repository repo) {
88  		super(repo);
89  		walk = new RevWalk(repo);
90  	}
91  
92  	/**
93  	 * {@inheritDoc}
94  	 * <p>
95  	 * Executes the {@code Log} command with all the options and parameters
96  	 * collected by the setter methods (e.g. {@link #add(AnyObjectId)},
97  	 * {@link #not(AnyObjectId)}, ..) of this class. Each instance of this class
98  	 * should only be used for one invocation of the command. Don't call this
99  	 * method twice on an instance.
100 	 */
101 	@Override
102 	public Iterable<RevCommit> call() throws GitAPIException, NoHeadException {
103 		checkCallable();
104 		List<TreeFilter> filters = new ArrayList<>();
105 		if (!pathFilters.isEmpty()) {
106 			filters.add(AndTreeFilter.create(PathFilterGroup.create(pathFilters), TreeFilter.ANY_DIFF));
107 		}
108 		if (!excludeTreeFilters.isEmpty()) {
109 			for (TreeFilter f : excludeTreeFilters) {
110 				filters.add(AndTreeFilter.create(f, TreeFilter.ANY_DIFF));
111 			}
112 		}
113 		if (!filters.isEmpty()) {
114 			if (filters.size() == 1) {
115 				filters.add(TreeFilter.ANY_DIFF);
116 			}
117 			walk.setTreeFilter(AndTreeFilter.create(filters));
118 
119 		}
120 		if (skip > -1 && maxCount > -1)
121 			walk.setRevFilter(AndRevFilter.create(SkipRevFilter.create(skip),
122 					MaxCountRevFilter.create(maxCount)));
123 		else if (skip > -1)
124 			walk.setRevFilter(SkipRevFilter.create(skip));
125 		else if (maxCount > -1)
126 			walk.setRevFilter(MaxCountRevFilter.create(maxCount));
127 		if (!startSpecified) {
128 			try {
129 				ObjectId headId = repo.resolve(Constants.HEAD);
130 				if (headId == null)
131 					throw new NoHeadException(
132 							JGitText.get().noHEADExistsAndNoExplicitStartingRevisionWasSpecified);
133 				add(headId);
134 			} catch (IOException e) {
135 				// all exceptions thrown by add() shouldn't occur and represent
136 				// severe low-level exception which are therefore wrapped
137 				throw new JGitInternalException(
138 						JGitText.get().anExceptionOccurredWhileTryingToAddTheIdOfHEAD,
139 						e);
140 			}
141 		}
142 
143 		if (this.revFilter != null) {
144 			walk.setRevFilter(this.revFilter);
145 		}
146 
147 		setCallable(false);
148 		return walk;
149 	}
150 
151 	/**
152 	 * Mark a commit to start graph traversal from.
153 	 *
154 	 * @see RevWalk#markStart(RevCommit)
155 	 * @param start
156 	 *            the id of the commit to start from
157 	 * @return {@code this}
158 	 * @throws org.eclipse.jgit.errors.MissingObjectException
159 	 *             the commit supplied is not available from the object
160 	 *             database. This usually indicates the supplied commit is
161 	 *             invalid, but the reference was constructed during an earlier
162 	 *             invocation to
163 	 *             {@link org.eclipse.jgit.revwalk.RevWalk#lookupCommit(AnyObjectId)}.
164 	 * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException
165 	 *             the object was not parsed yet and it was discovered during
166 	 *             parsing that it is not actually a commit. This usually
167 	 *             indicates the caller supplied a non-commit SHA-1 to
168 	 *             {@link org.eclipse.jgit.revwalk.RevWalk#lookupCommit(AnyObjectId)}.
169 	 * @throws JGitInternalException
170 	 *             a low-level exception of JGit has occurred. The original
171 	 *             exception can be retrieved by calling
172 	 *             {@link java.lang.Exception#getCause()}. Expect only
173 	 *             {@code IOException's} to be wrapped. Subclasses of
174 	 *             {@link java.io.IOException} (e.g.
175 	 *             {@link org.eclipse.jgit.errors.MissingObjectException}) are
176 	 *             typically not wrapped here but thrown as original exception
177 	 */
178 	public LogCommand add(AnyObjectId start) throws MissingObjectException,
179 			IncorrectObjectTypeException {
180 		return add(true, start);
181 	}
182 
183 	/**
184 	 * Same as {@code --not start}, or {@code ^start}
185 	 *
186 	 * @param start
187 	 *            a {@link org.eclipse.jgit.lib.AnyObjectId}
188 	 * @return {@code this}
189 	 * @throws org.eclipse.jgit.errors.MissingObjectException
190 	 *             the commit supplied is not available from the object
191 	 *             database. This usually indicates the supplied commit is
192 	 *             invalid, but the reference was constructed during an earlier
193 	 *             invocation to
194 	 *             {@link org.eclipse.jgit.revwalk.RevWalk#lookupCommit(AnyObjectId)}.
195 	 * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException
196 	 *             the object was not parsed yet and it was discovered during
197 	 *             parsing that it is not actually a commit. This usually
198 	 *             indicates the caller supplied a non-commit SHA-1 to
199 	 *             {@link org.eclipse.jgit.revwalk.RevWalk#lookupCommit(AnyObjectId)}.
200 	 * @throws JGitInternalException
201 	 *             a low-level exception of JGit has occurred. The original
202 	 *             exception can be retrieved by calling
203 	 *             {@link java.lang.Exception#getCause()}. Expect only
204 	 *             {@code IOException's} to be wrapped. Subclasses of
205 	 *             {@link java.io.IOException} (e.g.
206 	 *             {@link org.eclipse.jgit.errors.MissingObjectException}) are
207 	 *             typically not wrapped here but thrown as original exception
208 	 */
209 	public LogCommand not(AnyObjectId start) throws MissingObjectException,
210 			IncorrectObjectTypeException {
211 		return add(false, start);
212 	}
213 
214 	/**
215 	 * Adds the range {@code since..until}
216 	 *
217 	 * @param since
218 	 *            a {@link org.eclipse.jgit.lib.AnyObjectId} object.
219 	 * @param until
220 	 *            a {@link org.eclipse.jgit.lib.AnyObjectId} object.
221 	 * @return {@code this}
222 	 * @throws org.eclipse.jgit.errors.MissingObjectException
223 	 *             the commit supplied is not available from the object
224 	 *             database. This usually indicates the supplied commit is
225 	 *             invalid, but the reference was constructed during an earlier
226 	 *             invocation to
227 	 *             {@link org.eclipse.jgit.revwalk.RevWalk#lookupCommit(AnyObjectId)}.
228 	 * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException
229 	 *             the object was not parsed yet and it was discovered during
230 	 *             parsing that it is not actually a commit. This usually
231 	 *             indicates the caller supplied a non-commit SHA-1 to
232 	 *             {@link org.eclipse.jgit.revwalk.RevWalk#lookupCommit(AnyObjectId)}.
233 	 * @throws JGitInternalException
234 	 *             a low-level exception of JGit has occurred. The original
235 	 *             exception can be retrieved by calling
236 	 *             {@link java.lang.Exception#getCause()}. Expect only
237 	 *             {@code IOException's} to be wrapped. Subclasses of
238 	 *             {@link java.io.IOException} (e.g.
239 	 *             {@link org.eclipse.jgit.errors.MissingObjectException}) are
240 	 *             typically not wrapped here but thrown as original exception
241 	 */
242 	public LogCommand addRange(AnyObjectId since, AnyObjectId until)
243 			throws MissingObjectException, IncorrectObjectTypeException {
244 		return not(since).add(until);
245 	}
246 
247 	/**
248 	 * Add all refs as commits to start the graph traversal from.
249 	 *
250 	 * @see #add(AnyObjectId)
251 	 * @return {@code this}
252 	 * @throws java.io.IOException
253 	 *             the references could not be accessed
254 	 */
255 	public LogCommand all() throws IOException {
256 		for (Ref ref : getRepository().getRefDatabase().getRefs()) {
257 			if(!ref.isPeeled())
258 				ref = getRepository().getRefDatabase().peel(ref);
259 
260 			ObjectId objectId = ref.getPeeledObjectId();
261 			if (objectId == null)
262 				objectId = ref.getObjectId();
263 			RevCommit commit = null;
264 			try {
265 				commit = walk.parseCommit(objectId);
266 			} catch (MissingObjectException | IncorrectObjectTypeException e) {
267 				// ignore as traversal starting point:
268 				// - the ref points to an object that does not exist
269 				// - the ref points to an object that is not a commit (e.g. a
270 				// tree or a blob)
271 			}
272 			if (commit != null)
273 				add(commit);
274 		}
275 		return this;
276 	}
277 
278 	/**
279 	 * Show only commits that affect any of the specified paths. The path must
280 	 * either name a file or a directory exactly and use <code>/</code> (slash)
281 	 * as separator. Note that regex expressions or wildcards are not supported.
282 	 *
283 	 * @param path
284 	 *            a repository-relative path (with <code>/</code> as separator)
285 	 * @return {@code this}
286 	 */
287 	public LogCommand addPath(String path) {
288 		checkCallable();
289 		pathFilters.add(PathFilter.create(path));
290 		return this;
291 	}
292 
293 	/**
294 	 * Show all commits that are not within any of the specified paths. The path
295 	 * must either name a file or a directory exactly and use <code>/</code>
296 	 * (slash) as separator. Note that regular expressions or wildcards are not
297 	 * yet supported. If a path is both added and excluded from the search, then
298 	 * the exclusion wins.
299 	 *
300 	 * @param path
301 	 *            a repository-relative path (with <code>/</code> as separator)
302 	 * @return {@code this}
303 	 * @since 5.6
304 	 */
305 	public LogCommand excludePath(String path) {
306 		checkCallable();
307 		excludeTreeFilters.add(PathFilter.create(path).negate());
308 		return this;
309 	}
310 
311 	/**
312 	 * Skip the number of commits before starting to show the commit output.
313 	 *
314 	 * @param skip
315 	 *            the number of commits to skip
316 	 * @return {@code this}
317 	 */
318 	public LogCommand setSkip(int skip) {
319 		checkCallable();
320 		this.skip = skip;
321 		return this;
322 	}
323 
324 	/**
325 	 * Limit the number of commits to output.
326 	 *
327 	 * @param maxCount
328 	 *            the limit
329 	 * @return {@code this}
330 	 */
331 	public LogCommand setMaxCount(int maxCount) {
332 		checkCallable();
333 		this.maxCount = maxCount;
334 		return this;
335 	}
336 
337 	private LogCommand add(boolean include, AnyObjectId start)
338 			throws MissingObjectException, IncorrectObjectTypeException,
339 			JGitInternalException {
340 		checkCallable();
341 		try {
342 			if (include) {
343 				walk.markStart(walk.lookupCommit(start));
344 				startSpecified = true;
345 			} else
346 				walk.markUninteresting(walk.lookupCommit(start));
347 			return this;
348 		} catch (MissingObjectException | IncorrectObjectTypeException e) {
349 			throw e;
350 		} catch (IOException e) {
351 			throw new JGitInternalException(MessageFormat.format(
352 					JGitText.get().exceptionOccurredDuringAddingOfOptionToALogCommand
353 					, start), e);
354 		}
355 	}
356 
357 
358 	/**
359 	 * Set a filter for the <code>LogCommand</code>.
360 	 *
361 	 * @param aFilter
362 	 *            the filter that this instance of <code>LogCommand</code>
363 	 *            should use
364 	 * @return {@code this}
365 	 * @since 4.4
366 	 */
367 	public LogCommand setRevFilter(RevFilter aFilter) {
368 		checkCallable();
369 		this.revFilter = aFilter;
370 		return this;
371 	}
372 }