View Javadoc
1   /*
2    * Copyright (C) 2013, CloudBees, 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  package org.eclipse.jgit.api;
11  
12  import static org.eclipse.jgit.lib.Constants.R_TAGS;
13  
14  import java.io.IOException;
15  import java.text.MessageFormat;
16  import java.util.ArrayList;
17  import java.util.Collection;
18  import java.util.Collections;
19  import java.util.Comparator;
20  import java.util.Date;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.Optional;
24  import java.util.stream.Collectors;
25  import java.util.stream.Stream;
26  
27  import org.eclipse.jgit.api.errors.GitAPIException;
28  import org.eclipse.jgit.api.errors.JGitInternalException;
29  import org.eclipse.jgit.api.errors.RefNotFoundException;
30  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
31  import org.eclipse.jgit.errors.InvalidPatternException;
32  import org.eclipse.jgit.errors.MissingObjectException;
33  import org.eclipse.jgit.fnmatch.FileNameMatcher;
34  import org.eclipse.jgit.internal.JGitText;
35  import org.eclipse.jgit.lib.Constants;
36  import org.eclipse.jgit.lib.ObjectId;
37  import org.eclipse.jgit.lib.Ref;
38  import org.eclipse.jgit.lib.Repository;
39  import org.eclipse.jgit.revwalk.RevCommit;
40  import org.eclipse.jgit.revwalk.RevFlag;
41  import org.eclipse.jgit.revwalk.RevFlagSet;
42  import org.eclipse.jgit.revwalk.RevTag;
43  import org.eclipse.jgit.revwalk.RevWalk;
44  
45  /**
46   * Given a commit, show the most recent tag that is reachable from a commit.
47   *
48   * @since 3.2
49   */
50  public class DescribeCommand extends GitCommand<String> {
51  	private final RevWalk w;
52  
53  	/**
54  	 * Commit to describe.
55  	 */
56  	private RevCommit target;
57  
58  	/**
59  	 * How many tags we'll consider as candidates.
60  	 * This can only go up to the number of flags JGit can support in a walk,
61  	 * which is 24.
62  	 */
63  	private int maxCandidates = 10;
64  
65  	/**
66  	 * Whether to always use long output format or not.
67  	 */
68  	private boolean longDesc;
69  
70  	/**
71  	 * Pattern matchers to be applied to tags under consideration.
72  	 */
73  	private List<FileNameMatcher> matchers = new ArrayList<>();
74  
75  	/**
76  	 * Whether to use all tags (incl. lightweight) or not.
77  	 */
78  	private boolean useTags;
79  
80  	/**
81  	 * Whether to show a uniquely abbreviated commit hash as a fallback or not.
82  	 */
83  	private boolean always;
84  
85  	/**
86  	 * Constructor for DescribeCommand.
87  	 *
88  	 * @param repo
89  	 *            the {@link org.eclipse.jgit.lib.Repository}
90  	 */
91  	protected DescribeCommand(Repository repo) {
92  		super(repo);
93  		w = new RevWalk(repo);
94  		w.setRetainBody(false);
95  	}
96  
97  	/**
98  	 * Sets the commit to be described.
99  	 *
100 	 * @param target
101 	 * 		A non-null object ID to be described.
102 	 * @return {@code this}
103 	 * @throws MissingObjectException
104 	 *             the supplied commit does not exist.
105 	 * @throws IncorrectObjectTypeException
106 	 *             the supplied id is not a commit or an annotated tag.
107 	 * @throws java.io.IOException
108 	 *             a pack file or loose object could not be read.
109 	 */
110 	public DescribeCommand setTarget(ObjectId target) throws IOException {
111 		this.target = w.parseCommit(target);
112 		return this;
113 	}
114 
115 	/**
116 	 * Sets the commit to be described.
117 	 *
118 	 * @param rev
119 	 *            Commit ID, tag, branch, ref, etc. See
120 	 *            {@link org.eclipse.jgit.lib.Repository#resolve(String)} for
121 	 *            allowed syntax.
122 	 * @return {@code this}
123 	 * @throws IncorrectObjectTypeException
124 	 *             the supplied id is not a commit or an annotated tag.
125 	 * @throws org.eclipse.jgit.api.errors.RefNotFoundException
126 	 *             the given rev didn't resolve to any object.
127 	 * @throws java.io.IOException
128 	 *             a pack file or loose object could not be read.
129 	 */
130 	public DescribeCommand setTarget(String rev) throws IOException,
131 			RefNotFoundException {
132 		ObjectId id = repo.resolve(rev);
133 		if (id == null)
134 			throw new RefNotFoundException(MessageFormat.format(JGitText.get().refNotResolved, rev));
135 		return setTarget(id);
136 	}
137 
138 	/**
139 	 * Determine whether always to use the long format or not. When set to
140 	 * <code>true</code> the long format is used even the commit matches a tag.
141 	 *
142 	 * @param longDesc
143 	 *            <code>true</code> if always the long format should be used.
144 	 * @return {@code this}
145 	 * @see <a
146 	 *      href="https://www.kernel.org/pub/software/scm/git/docs/git-describe.html"
147 	 *      >Git documentation about describe</a>
148 	 * @since 4.0
149 	 */
150 	public DescribeCommand setLong(boolean longDesc) {
151 		this.longDesc = longDesc;
152 		return this;
153 	}
154 
155 	/**
156 	 * Instead of using only the annotated tags, use any tag found in refs/tags
157 	 * namespace. This option enables matching lightweight (non-annotated) tags
158 	 * or not.
159 	 *
160 	 * @param tags
161 	 *            <code>true</code> enables matching lightweight (non-annotated)
162 	 *            tags like setting option --tags in c git
163 	 * @return {@code this}
164 	 * @since 5.0
165 	 */
166 	public DescribeCommand setTags(boolean tags) {
167 		this.useTags = tags;
168 		return this;
169 	}
170 
171 	/**
172 	 * Always describe the commit by eventually falling back to a uniquely
173 	 * abbreviated commit hash if no other name matches.
174 	 *
175 	 * @param always
176 	 *            <code>true</code> enables falling back to a uniquely
177 	 *            abbreviated commit hash
178 	 * @return {@code this}
179 	 * @since 5.4
180 	 */
181 	public DescribeCommand setAlways(boolean always) {
182 		this.always = always;
183 		return this;
184 	}
185 
186 	private String longDescription(Ref tag, int depth, ObjectId tip)
187 			throws IOException {
188 		return String.format(
189 				"%s-%d-g%s", tag.getName().substring(R_TAGS.length()), //$NON-NLS-1$
190 				Integer.valueOf(depth), w.getObjectReader().abbreviate(tip)
191 						.name());
192 	}
193 
194 	/**
195 	 * Sets one or more {@code glob(7)} patterns that tags must match to be
196 	 * considered. If multiple patterns are provided, tags only need match one
197 	 * of them.
198 	 *
199 	 * @param patterns
200 	 *            the {@code glob(7)} pattern or patterns
201 	 * @return {@code this}
202 	 * @throws org.eclipse.jgit.errors.InvalidPatternException
203 	 *             if the pattern passed in was invalid.
204 	 * @see <a href=
205 	 *      "https://www.kernel.org/pub/software/scm/git/docs/git-describe.html"
206 	 *      >Git documentation about describe</a>
207 	 * @since 4.9
208 	 */
209 	public DescribeCommand setMatch(String... patterns) throws InvalidPatternException {
210 		for (String p : patterns) {
211 			matchers.add(new FileNameMatcher(p, null));
212 		}
213 		return this;
214 	}
215 
216 	private final Comparator<Ref> TAG_TIE_BREAKER = new Comparator<Ref>() {
217 
218 		@Override
219 		public int compare(Reff" href="../../../../org/eclipse/jgit/lib/Ref.html#Ref">Ref o1, Ref o2) {
220 			try {
221 				return tagDate(o2).compareTo(tagDate(o1));
222 			} catch (IOException e) {
223 				return 0;
224 			}
225 		}
226 
227 		private Date tagDate(Ref tag) throws IOException {
228 			RevTag t = w.parseTag(tag.getObjectId());
229 			w.parseBody(t);
230 			return t.getTaggerIdent().getWhen();
231 		}
232 	};
233 
234 	private Optional<Ref> getBestMatch(List<Ref> tags) {
235 		if (tags == null || tags.isEmpty()) {
236 			return Optional.empty();
237 		} else if (matchers.isEmpty()) {
238 			Collections.sort(tags, TAG_TIE_BREAKER);
239 			return Optional.of(tags.get(0));
240 		} else {
241 			// Find the first tag that matches in the stream of all tags
242 			// filtered by matchers ordered by tie break order
243 			Stream<Ref> matchingTags = Stream.empty();
244 			for (FileNameMatcher matcher : matchers) {
245 				Stream<Ref> m = tags.stream().filter(
246 						tag -> {
247 							matcher.append(
248 									tag.getName().substring(R_TAGS.length()));
249 							boolean result = matcher.isMatch();
250 							matcher.reset();
251 							return result;
252 						});
253 				matchingTags = Stream.of(matchingTags, m).flatMap(i -> i);
254 			}
255 			return matchingTags.sorted(TAG_TIE_BREAKER).findFirst();
256 		}
257 	}
258 
259 	private ObjectId getObjectIdFromRef(Ref r) throws JGitInternalException {
260 		try {
261 			ObjectId key = repo.getRefDatabase().peel(r).getPeeledObjectId();
262 			if (key == null) {
263 				key = r.getObjectId();
264 			}
265 			return key;
266 		} catch (IOException e) {
267 			throw new JGitInternalException(e.getMessage(), e);
268 		}
269 	}
270 
271 	/**
272 	 * {@inheritDoc}
273 	 * <p>
274 	 * Describes the specified commit. Target defaults to HEAD if no commit was
275 	 * set explicitly.
276 	 */
277 	@Override
278 	public String call() throws GitAPIException {
279 		try {
280 			checkCallable();
281 			if (target == null) {
282 				setTarget(Constants.HEAD);
283 			}
284 
285 			Collection<Ref> tagList = repo.getRefDatabase()
286 					.getRefsByPrefix(R_TAGS);
287 			Map<ObjectId, List<Ref>> tags = tagList.stream()
288 					.filter(this::filterLightweightTags)
289 					.collect(Collectors.groupingBy(this::getObjectIdFromRef));
290 
291 			// combined flags of all the candidate instances
292 			final RevFlagSett.html#RevFlagSet">RevFlagSet allFlags = new RevFlagSet();
293 
294 			/**
295 			 * Tracks the depth of each tag as we find them.
296 			 */
297 			class Candidate {
298 				final Ref tag;
299 				final RevFlag flag;
300 
301 				/**
302 				 * This field counts number of commits that are reachable from
303 				 * the tip but not reachable from the tag.
304 				 */
305 				int depth;
306 
307 				Candidate(RevCommit commit, Ref tag) {
308 					this.tag = tag;
309 					this.flag = w.newFlag(tag.getName());
310 					// we'll mark all the nodes reachable from this tag accordingly
311 					allFlags.add(flag);
312 					w.carry(flag);
313 					commit.add(flag);
314 					// As of this writing, JGit carries a flag from a child to its parents
315 					// right before RevWalk.next() returns, so all the flags that are added
316 					// must be manually carried to its parents. If that gets fixed,
317 					// this will be unnecessary.
318 					commit.carry(flag);
319 				}
320 
321 				/**
322 				 * Does this tag contain the given commit?
323 				 */
324 				boolean reaches(RevCommit c) {
325 					return c.has(flag);
326 				}
327 
328 				String describe(ObjectId tip) throws IOException {
329 					return longDescription(tag, depth, tip);
330 				}
331 
332 			}
333 			List<Candidate> candidates = new ArrayList<>();    // all the candidates we find
334 
335 			// is the target already pointing to a suitable tag? if so, we are done!
336 			Optional<Ref> bestMatch = getBestMatch(tags.get(target));
337 			if (bestMatch.isPresent()) {
338 				return longDesc ? longDescription(bestMatch.get(), 0, target) :
339 						bestMatch.get().getName().substring(R_TAGS.length());
340 			}
341 
342 			w.markStart(target);
343 
344 			int seen = 0;   // commit seen thus far
345 			RevCommit c;
346 			while ((c = w.next()) != null) {
347 				if (!c.hasAny(allFlags)) {
348 					// if a tag already dominates this commit,
349 					// then there's no point in picking a tag on this commit
350 					// since the one that dominates it is always more preferable
351 					bestMatch = getBestMatch(tags.get(c));
352 					if (bestMatch.isPresent()) {
353 						Candidate cd = new Candidate(c, bestMatch.get());
354 						candidates.add(cd);
355 						cd.depth = seen;
356 					}
357 				}
358 
359 				// if the newly discovered commit isn't reachable from a tag that we've seen
360 				// it counts toward the total depth.
361 				for (Candidate cd : candidates) {
362 					if (!cd.reaches(c))
363 						cd.depth++;
364 				}
365 
366 				// if we have search going for enough tags, we will start
367 				// closing down. JGit can only give us a finite number of bits,
368 				// so we can't track all tags even if we wanted to.
369 				if (candidates.size() >= maxCandidates)
370 					break;
371 
372 				// TODO: if all the commits in the queue of RevWalk has allFlags
373 				// there's no point in continuing search as we'll not discover any more
374 				// tags. But RevWalk doesn't expose this.
375 				seen++;
376 			}
377 
378 			// at this point we aren't adding any more tags to our search,
379 			// but we still need to count all the depths correctly.
380 			while ((c = w.next()) != null) {
381 				if (c.hasAll(allFlags)) {
382 					// no point in visiting further from here, so cut the search here
383 					for (RevCommit p : c.getParents())
384 						p.add(RevFlag.SEEN);
385 				} else {
386 					for (Candidate cd : candidates) {
387 						if (!cd.reaches(c))
388 							cd.depth++;
389 					}
390 				}
391 			}
392 
393 			// if all the nodes are dominated by all the tags, the walk stops
394 			if (candidates.isEmpty()) {
395 				return always ? w.getObjectReader().abbreviate(target).name() : null;
396 			}
397 
398 			Candidate best = Collections.min(candidates,
399 					(Candidate o1, Candidate o2) -> o1.depth - o2.depth);
400 
401 			return best.describe(target);
402 		} catch (IOException e) {
403 			throw new JGitInternalException(e.getMessage(), e);
404 		} finally {
405 			setCallable(false);
406 			w.close();
407 		}
408 	}
409 
410 	/**
411 	 * Whether we use lightweight tags or not for describe Candidates
412 	 *
413 	 * @param ref
414 	 *            reference under inspection
415 	 * @return true if it should be used for describe or not regarding
416 	 *         {@link org.eclipse.jgit.api.DescribeCommand#useTags}
417 	 */
418 	@SuppressWarnings("null")
419 	private boolean filterLightweightTags(Ref ref) {
420 		ObjectId id = ref.getObjectId();
421 		try {
422 			return this.useTags || (id != null && (w.parseTag(id) != null));
423 		} catch (IOException e) {
424 			return false;
425 		}
426 	}
427 }