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