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