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