View Javadoc
1   /*
2    * Copyright (C) 2013, Google Inc.
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  
44  package org.eclipse.jgit.api;
45  
46  import java.io.IOException;
47  import java.util.ArrayList;
48  import java.util.HashMap;
49  import java.util.LinkedHashMap;
50  import java.util.List;
51  import java.util.Map;
52  
53  import org.eclipse.jgit.api.errors.GitAPIException;
54  import org.eclipse.jgit.api.errors.JGitInternalException;
55  import org.eclipse.jgit.errors.MissingObjectException;
56  import org.eclipse.jgit.lib.AnyObjectId;
57  import org.eclipse.jgit.lib.Constants;
58  import org.eclipse.jgit.lib.ObjectId;
59  import org.eclipse.jgit.lib.Ref;
60  import org.eclipse.jgit.lib.Repository;
61  import org.eclipse.jgit.revwalk.FIFORevQueue;
62  import org.eclipse.jgit.revwalk.RevCommit;
63  import org.eclipse.jgit.revwalk.RevObject;
64  import org.eclipse.jgit.revwalk.RevTag;
65  import org.eclipse.jgit.revwalk.RevWalk;
66  
67  /**
68   * Command to find human-readable names of revisions.
69   *
70   * @see <a
71   *      href="http://www.kernel.org/pub/software/scm/git/docs/git-name-rev.html"
72   *      >Git documentation about name-rev</a>
73   * @since 3.0
74   */
75  public class NameRevCommand extends GitCommand<Map<ObjectId, String>> {
76  	/** Amount of slop to allow walking past the earliest requested commit. */
77  	private static final int COMMIT_TIME_SLOP = 60 * 60 * 24;
78  
79  	/** Cost of traversing a merge commit compared to a linear history. */
80  	private static final int MERGE_COST = 65535;
81  
82  	private static class NameRevCommit extends RevCommit {
83  		private String tip;
84  		private int distance;
85  		private long cost;
86  
87  		private NameRevCommit(AnyObjectId id) {
88  			super(id);
89  		}
90  
91  		private StringBuilder format() {
92  			StringBuilder sb = new StringBuilder(tip);
93  			if (distance > 0)
94  				sb.append('~').append(distance);
95  			return sb;
96  		}
97  
98  		@Override
99  		public String toString() {
100 			StringBuilder sb = new StringBuilder(getClass().getSimpleName())
101 				.append('[');
102 			if (tip != null)
103 				sb.append(format());
104 			else
105 				sb.append((Object) null);
106 			sb.append(',').append(cost).append(']').append(' ')
107 				.append(super.toString()).toString();
108 			return sb.toString();
109 		}
110 	}
111 
112 	private final RevWalk walk;
113 	private final List<String> prefixes;
114 	private final List<ObjectId> revs;
115 	private List<Ref> refs;
116 	private int mergeCost;
117 
118 	/**
119 	 * Create a new name-rev command.
120 	 *
121 	 * @param repo
122 	 *            the {@link org.eclipse.jgit.lib.Repository}
123 	 */
124 	protected NameRevCommand(Repository repo) {
125 		super(repo);
126 		mergeCost = MERGE_COST;
127 		prefixes = new ArrayList<>(2);
128 		revs = new ArrayList<>(2);
129 		walk = new RevWalk(repo) {
130 			@Override
131 			public NameRevCommit createCommit(AnyObjectId id) {
132 				return new NameRevCommit(id);
133 			}
134 		};
135 	}
136 
137 	/** {@inheritDoc} */
138 	@Override
139 	public Map<ObjectId, String> call() throws GitAPIException {
140 		try {
141 			Map<ObjectId, String> nonCommits = new HashMap<>();
142 			FIFORevQueue pending = new FIFORevQueue();
143 			if (refs != null) {
144 				for (Ref ref : refs)
145 					addRef(ref, nonCommits, pending);
146 			}
147 			addPrefixes(nonCommits, pending);
148 			int cutoff = minCommitTime() - COMMIT_TIME_SLOP;
149 
150 			while (true) {
151 				NameRevCommit c = (NameRevCommit) pending.next();
152 				if (c == null)
153 					break;
154 				if (c.getCommitTime() < cutoff)
155 					continue;
156 				for (int i = 0; i < c.getParentCount(); i++) {
157 					NameRevCommit p = (NameRevCommit) walk.parseCommit(c.getParent(i));
158 					long cost = c.cost + (i > 0 ? mergeCost : 1);
159 					if (p.tip == null || compare(c.tip, cost, p.tip, p.cost) < 0) {
160 						if (i > 0) {
161 							p.tip = c.format().append('^').append(i + 1).toString();
162 							p.distance = 0;
163 						} else {
164 							p.tip = c.tip;
165 							p.distance = c.distance + 1;
166 						}
167 						p.cost = cost;
168 						pending.add(p);
169 					}
170 				}
171 			}
172 
173 			Map<ObjectId, String> result =
174 				new LinkedHashMap<>(revs.size());
175 			for (ObjectId id : revs) {
176 				RevObject o = walk.parseAny(id);
177 				if (o instanceof NameRevCommit) {
178 					NameRevCommit c = (NameRevCommit) o;
179 					if (c.tip != null)
180 						result.put(id, simplify(c.format().toString()));
181 				} else {
182 					String name = nonCommits.get(id);
183 					if (name != null)
184 						result.put(id, simplify(name));
185 				}
186 			}
187 
188 			setCallable(false);
189 			return result;
190 		} catch (IOException e) {
191 			throw new JGitInternalException(e.getMessage(), e);
192 		} finally {
193 			walk.close();
194 		}
195 	}
196 
197 	/**
198 	 * Add an object to search for.
199 	 *
200 	 * @param id
201 	 *            object ID to add.
202 	 * @return {@code this}
203 	 * @throws org.eclipse.jgit.errors.MissingObjectException
204 	 *             the object supplied is not available from the object
205 	 *             database.
206 	 * @throws org.eclipse.jgit.api.errors.JGitInternalException
207 	 *             a low-level exception of JGit has occurred. The original
208 	 *             exception can be retrieved by calling
209 	 *             {@link java.lang.Exception#getCause()}.
210 	 */
211 	public NameRevCommand add(ObjectId id) throws MissingObjectException,
212 			JGitInternalException {
213 		checkCallable();
214 		try {
215 			walk.parseAny(id);
216 		} catch (MissingObjectException e) {
217 			throw e;
218 		} catch (IOException e) {
219 			throw new JGitInternalException(e.getMessage(), e);
220 		}
221 		revs.add(id.copy());
222 		return this;
223 	}
224 
225 	/**
226 	 * Add multiple objects to search for.
227 	 *
228 	 * @param ids
229 	 *            object IDs to add.
230 	 * @return {@code this}
231 	 * @throws org.eclipse.jgit.errors.MissingObjectException
232 	 *             the object supplied is not available from the object
233 	 *             database.
234 	 * @throws org.eclipse.jgit.api.errors.JGitInternalException
235 	 *             a low-level exception of JGit has occurred. The original
236 	 *             exception can be retrieved by calling
237 	 *             {@link java.lang.Exception#getCause()}.
238 	 */
239 	public NameRevCommand add(Iterable<ObjectId> ids)
240 			throws MissingObjectException, JGitInternalException {
241 		for (ObjectId id : ids)
242 			add(id);
243 		return this;
244 	}
245 
246 	/**
247 	 * Add a ref prefix to the set that results must match.
248 	 * <p>
249 	 * If an object matches multiple refs equally well, the first matching ref
250 	 * added with {@link #addRef(Ref)} is preferred, or else the first matching
251 	 * prefix added by {@link #addPrefix(String)}.
252 	 *
253 	 * @param prefix
254 	 *            prefix to add; see
255 	 *            {@link org.eclipse.jgit.lib.RefDatabase#getRefs(String)}
256 	 * @return {@code this}
257 	 */
258 	public NameRevCommand addPrefix(String prefix) {
259 		checkCallable();
260 		prefixes.add(prefix);
261 		return this;
262 	}
263 
264 	/**
265 	 * Add all annotated tags under {@code refs/tags/} to the set that all
266 	 * results must match.
267 	 * <p>
268 	 * Calls {@link #addRef(Ref)}; see that method for a note on matching
269 	 * priority.
270 	 *
271 	 * @return {@code this}
272 	 * @throws JGitInternalException
273 	 *             a low-level exception of JGit has occurred. The original
274 	 *             exception can be retrieved by calling
275 	 *             {@link java.lang.Exception#getCause()}.
276 	 */
277 	public NameRevCommand addAnnotatedTags() {
278 		checkCallable();
279 		if (refs == null)
280 			refs = new ArrayList<>();
281 		try {
282 			for (Ref ref : repo.getRefDatabase().getRefs(Constants.R_TAGS).values()) {
283 				ObjectId id = ref.getObjectId();
284 				if (id != null && (walk.parseAny(id) instanceof RevTag))
285 					addRef(ref);
286 			}
287 		} catch (IOException e) {
288 			throw new JGitInternalException(e.getMessage(), e);
289 		}
290 		return this;
291 	}
292 
293 	/**
294 	 * Add a ref to the set that all results must match.
295 	 * <p>
296 	 * If an object matches multiple refs equally well, the first matching ref
297 	 * added with {@link #addRef(Ref)} is preferred, or else the first matching
298 	 * prefix added by {@link #addPrefix(String)}.
299 	 *
300 	 * @param ref
301 	 *            ref to add.
302 	 * @return {@code this}
303 	 */
304 	public NameRevCommand addRef(Ref ref) {
305 		checkCallable();
306 		if (refs == null)
307 			refs = new ArrayList<>();
308 		refs.add(ref);
309 		return this;
310 	}
311 
312 	NameRevCommand setMergeCost(int cost) {
313 		mergeCost = cost;
314 		return this;
315 	}
316 
317 	private void addPrefixes(Map<ObjectId, String> nonCommits,
318 			FIFORevQueue pending) throws IOException {
319 		if (!prefixes.isEmpty()) {
320 			for (String prefix : prefixes)
321 				addPrefix(prefix, nonCommits, pending);
322 		} else if (refs == null)
323 			addPrefix(Constants.R_REFS, nonCommits, pending);
324 	}
325 
326 	private void addPrefix(String prefix, Map<ObjectId, String> nonCommits,
327 			FIFORevQueue pending) throws IOException {
328 		for (Ref ref : repo.getRefDatabase().getRefs(prefix).values())
329 			addRef(ref, nonCommits, pending);
330 	}
331 
332 	private void addRef(Ref ref, Map<ObjectId, String> nonCommits,
333 			FIFORevQueue pending) throws IOException {
334 		if (ref.getObjectId() == null)
335 			return;
336 		RevObject o = walk.parseAny(ref.getObjectId());
337 		while (o instanceof RevTag) {
338 			RevTag t = (RevTag) o;
339 			nonCommits.put(o, ref.getName());
340 			o = t.getObject();
341 			walk.parseHeaders(o);
342 		}
343 		if (o instanceof NameRevCommit) {
344 			NameRevCommit c = (NameRevCommit) o;
345 			if (c.tip == null)
346 				c.tip = ref.getName();
347 			pending.add(c);
348 		} else if (!nonCommits.containsKey(o))
349 			nonCommits.put(o, ref.getName());
350 	}
351 
352 	private int minCommitTime() throws IOException {
353 		int min = Integer.MAX_VALUE;
354 		for (ObjectId id : revs) {
355 			RevObject o = walk.parseAny(id);
356 			while (o instanceof RevTag) {
357 				o = ((RevTag) o).getObject();
358 				walk.parseHeaders(o);
359 			}
360 			if (o instanceof RevCommit) {
361 				RevCommit c = (RevCommit) o;
362 				if (c.getCommitTime() < min)
363 					min = c.getCommitTime();
364 			}
365 		}
366 		return min;
367 	}
368 
369 	private long compare(String leftTip, long leftCost, String rightTip, long rightCost) {
370 		long c = leftCost - rightCost;
371 		if (c != 0 || prefixes.isEmpty())
372 			return c;
373 		int li = -1;
374 		int ri = -1;
375 		for (int i = 0; i < prefixes.size(); i++) {
376 			String prefix = prefixes.get(i);
377 			if (li < 0 && leftTip.startsWith(prefix))
378 				li = i;
379 			if (ri < 0 && rightTip.startsWith(prefix))
380 				ri = i;
381 		}
382 		// Don't tiebreak if prefixes are the same, in order to prefer first-parent
383 		// paths.
384 		return li - ri;
385 	}
386 
387 	private static String simplify(String refName) {
388 		if (refName.startsWith(Constants.R_HEADS))
389 			return refName.substring(Constants.R_HEADS.length());
390 		if (refName.startsWith(Constants.R_TAGS))
391 			return refName.substring(Constants.R_TAGS.length());
392 		if (refName.startsWith(Constants.R_REFS))
393 			return refName.substring(Constants.R_REFS.length());
394 		return refName;
395 	}
396 }