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