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<RevCommit> 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("dir/filename.txt").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 }