DescribeCommand.java

  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. import static org.eclipse.jgit.lib.Constants.R_REFS;
  12. import static org.eclipse.jgit.lib.Constants.R_TAGS;

  13. import java.io.IOException;
  14. import java.text.MessageFormat;
  15. import java.util.ArrayList;
  16. import java.util.Collection;
  17. import java.util.Collections;
  18. import java.util.Comparator;
  19. import java.util.Date;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.Optional;
  23. import java.util.stream.Collectors;
  24. import java.util.stream.Stream;

  25. import org.eclipse.jgit.api.errors.GitAPIException;
  26. import org.eclipse.jgit.api.errors.JGitInternalException;
  27. import org.eclipse.jgit.api.errors.RefNotFoundException;
  28. import org.eclipse.jgit.errors.IncorrectObjectTypeException;
  29. import org.eclipse.jgit.errors.InvalidPatternException;
  30. import org.eclipse.jgit.errors.MissingObjectException;
  31. import org.eclipse.jgit.fnmatch.FileNameMatcher;
  32. import org.eclipse.jgit.internal.JGitText;
  33. import org.eclipse.jgit.lib.Constants;
  34. import org.eclipse.jgit.lib.ObjectId;
  35. import org.eclipse.jgit.lib.Ref;
  36. import org.eclipse.jgit.lib.Repository;
  37. import org.eclipse.jgit.revwalk.RevCommit;
  38. import org.eclipse.jgit.revwalk.RevFlag;
  39. import org.eclipse.jgit.revwalk.RevFlagSet;
  40. import org.eclipse.jgit.revwalk.RevTag;
  41. import org.eclipse.jgit.revwalk.RevWalk;

  42. /**
  43.  * Given a commit, show the most recent tag that is reachable from a commit.
  44.  *
  45.  * @since 3.2
  46.  */
  47. public class DescribeCommand extends GitCommand<String> {
  48.     private final RevWalk w;

  49.     /**
  50.      * Commit to describe.
  51.      */
  52.     private RevCommit target;

  53.     /**
  54.      * How many tags we'll consider as candidates.
  55.      * This can only go up to the number of flags JGit can support in a walk,
  56.      * which is 24.
  57.      */
  58.     private int maxCandidates = 10;

  59.     /**
  60.      * Whether to always use long output format or not.
  61.      */
  62.     private boolean longDesc;

  63.     /**
  64.      * Pattern matchers to be applied to tags under consideration.
  65.      */
  66.     private List<FileNameMatcher> matchers = new ArrayList<>();

  67.     /**
  68.      * Whether to use all refs in the refs/ namespace
  69.      */
  70.     private boolean useAll;

  71.     /**
  72.      * Whether to use all tags (incl. lightweight) or not.
  73.      */
  74.     private boolean useTags;

  75.     /**
  76.      * Whether to show a uniquely abbreviated commit hash as a fallback or not.
  77.      */
  78.     private boolean always;

  79.     /**
  80.      * Constructor for DescribeCommand.
  81.      *
  82.      * @param repo
  83.      *            the {@link org.eclipse.jgit.lib.Repository}
  84.      */
  85.     protected DescribeCommand(Repository repo) {
  86.         super(repo);
  87.         w = new RevWalk(repo);
  88.         w.setRetainBody(false);
  89.     }

  90.     /**
  91.      * Sets the commit to be described.
  92.      *
  93.      * @param target
  94.      *      A non-null object ID to be described.
  95.      * @return {@code this}
  96.      * @throws MissingObjectException
  97.      *             the supplied commit does not exist.
  98.      * @throws IncorrectObjectTypeException
  99.      *             the supplied id is not a commit or an annotated tag.
  100.      * @throws java.io.IOException
  101.      *             a pack file or loose object could not be read.
  102.      */
  103.     public DescribeCommand setTarget(ObjectId target) throws IOException {
  104.         this.target = w.parseCommit(target);
  105.         return this;
  106.     }

  107.     /**
  108.      * Sets the commit to be described.
  109.      *
  110.      * @param rev
  111.      *            Commit ID, tag, branch, ref, etc. See
  112.      *            {@link org.eclipse.jgit.lib.Repository#resolve(String)} for
  113.      *            allowed syntax.
  114.      * @return {@code this}
  115.      * @throws IncorrectObjectTypeException
  116.      *             the supplied id is not a commit or an annotated tag.
  117.      * @throws org.eclipse.jgit.api.errors.RefNotFoundException
  118.      *             the given rev didn't resolve to any object.
  119.      * @throws java.io.IOException
  120.      *             a pack file or loose object could not be read.
  121.      */
  122.     public DescribeCommand setTarget(String rev) throws IOException,
  123.             RefNotFoundException {
  124.         ObjectId id = repo.resolve(rev);
  125.         if (id == null)
  126.             throw new RefNotFoundException(MessageFormat.format(JGitText.get().refNotResolved, rev));
  127.         return setTarget(id);
  128.     }

  129.     /**
  130.      * Determine whether always to use the long format or not. When set to
  131.      * <code>true</code> the long format is used even the commit matches a tag.
  132.      *
  133.      * @param longDesc
  134.      *            <code>true</code> if always the long format should be used.
  135.      * @return {@code this}
  136.      * @see <a
  137.      *      href="https://www.kernel.org/pub/software/scm/git/docs/git-describe.html"
  138.      *      >Git documentation about describe</a>
  139.      * @since 4.0
  140.      */
  141.     public DescribeCommand setLong(boolean longDesc) {
  142.         this.longDesc = longDesc;
  143.         return this;
  144.     }

  145.     /**
  146.      * Instead of using only the annotated tags, use any ref found in refs/
  147.      * namespace. This option enables matching any known branch,
  148.      * remote-tracking branch, or lightweight tag.
  149.      *
  150.      * @param all
  151.      *            <code>true</code> enables matching any ref found in refs/
  152.      *            like setting option --all in c git
  153.      * @return {@code this}
  154.      * @since 5.10
  155.      */
  156.     public DescribeCommand setAll(boolean all) {
  157.         this.useAll = all;
  158.         return this;
  159.     }

  160.     /**
  161.      * Instead of using only the annotated tags, use any tag found in refs/tags
  162.      * namespace. This option enables matching lightweight (non-annotated) tags
  163.      * or not.
  164.      *
  165.      * @param tags
  166.      *            <code>true</code> enables matching lightweight (non-annotated)
  167.      *            tags like setting option --tags in c git
  168.      * @return {@code this}
  169.      * @since 5.0
  170.      */
  171.     public DescribeCommand setTags(boolean tags) {
  172.         this.useTags = tags;
  173.         return this;
  174.     }

  175.     /**
  176.      * Always describe the commit by eventually falling back to a uniquely
  177.      * abbreviated commit hash if no other name matches.
  178.      *
  179.      * @param always
  180.      *            <code>true</code> enables falling back to a uniquely
  181.      *            abbreviated commit hash
  182.      * @return {@code this}
  183.      * @since 5.4
  184.      */
  185.     public DescribeCommand setAlways(boolean always) {
  186.         this.always = always;
  187.         return this;
  188.     }

  189.     private String longDescription(Ref tag, int depth, ObjectId tip)
  190.             throws IOException {
  191.         return String.format(
  192.                 "%s-%d-g%s", formatRefName(tag.getName()), //$NON-NLS-1$
  193.                 Integer.valueOf(depth), w.getObjectReader().abbreviate(tip)
  194.                         .name());
  195.     }

  196.     /**
  197.      * Sets one or more {@code glob(7)} patterns that tags must match to be
  198.      * considered. If multiple patterns are provided, tags only need match one
  199.      * of them.
  200.      *
  201.      * @param patterns
  202.      *            the {@code glob(7)} pattern or patterns
  203.      * @return {@code this}
  204.      * @throws org.eclipse.jgit.errors.InvalidPatternException
  205.      *             if the pattern passed in was invalid.
  206.      * @see <a href=
  207.      *      "https://www.kernel.org/pub/software/scm/git/docs/git-describe.html"
  208.      *      >Git documentation about describe</a>
  209.      * @since 4.9
  210.      */
  211.     public DescribeCommand setMatch(String... patterns) throws InvalidPatternException {
  212.         for (String p : patterns) {
  213.             matchers.add(new FileNameMatcher(p, null));
  214.         }
  215.         return this;
  216.     }

  217.     private final Comparator<Ref> TAG_TIE_BREAKER = new Comparator<Ref>() {

  218.         @Override
  219.         public int compare(Ref o1, Ref o2) {
  220.             try {
  221.                 return tagDate(o2).compareTo(tagDate(o1));
  222.             } catch (IOException e) {
  223.                 return 0;
  224.             }
  225.         }

  226.         private Date tagDate(Ref tag) throws IOException {
  227.             RevTag t = w.parseTag(tag.getObjectId());
  228.             w.parseBody(t);
  229.             return t.getTaggerIdent().getWhen();
  230.         }
  231.     };

  232.     private Optional<Ref> getBestMatch(List<Ref> tags) {
  233.         if (tags == null || tags.isEmpty()) {
  234.             return Optional.empty();
  235.         } else if (matchers.isEmpty()) {
  236.             Collections.sort(tags, TAG_TIE_BREAKER);
  237.             return Optional.of(tags.get(0));
  238.         } else {
  239.             // Find the first tag that matches in the stream of all tags
  240.             // filtered by matchers ordered by tie break order
  241.             Stream<Ref> matchingTags = Stream.empty();
  242.             for (FileNameMatcher matcher : matchers) {
  243.                 Stream<Ref> m = tags.stream().filter(
  244.                         tag -> {
  245.                             matcher.append(formatRefName(tag.getName()));
  246.                             boolean result = matcher.isMatch();
  247.                             matcher.reset();
  248.                             return result;
  249.                         });
  250.                 matchingTags = Stream.of(matchingTags, m).flatMap(i -> i);
  251.             }
  252.             return matchingTags.sorted(TAG_TIE_BREAKER).findFirst();
  253.         }
  254.     }

  255.     private ObjectId getObjectIdFromRef(Ref r) throws JGitInternalException {
  256.         try {
  257.             ObjectId key = repo.getRefDatabase().peel(r).getPeeledObjectId();
  258.             if (key == null) {
  259.                 key = r.getObjectId();
  260.             }
  261.             return key;
  262.         } catch (IOException e) {
  263.             throw new JGitInternalException(e.getMessage(), e);
  264.         }
  265.     }

  266.     /**
  267.      * {@inheritDoc}
  268.      * <p>
  269.      * Describes the specified commit. Target defaults to HEAD if no commit was
  270.      * set explicitly.
  271.      */
  272.     @Override
  273.     public String call() throws GitAPIException {
  274.         try {
  275.             checkCallable();
  276.             if (target == null) {
  277.                 setTarget(Constants.HEAD);
  278.             }

  279.             Collection<Ref> tagList = repo.getRefDatabase()
  280.                     .getRefsByPrefix(useAll ? R_REFS : R_TAGS);
  281.             Map<ObjectId, List<Ref>> tags = tagList.stream()
  282.                     .filter(this::filterLightweightTags)
  283.                     .collect(Collectors.groupingBy(this::getObjectIdFromRef));

  284.             // combined flags of all the candidate instances
  285.             final RevFlagSet allFlags = new RevFlagSet();

  286.             /**
  287.              * Tracks the depth of each tag as we find them.
  288.              */
  289.             class Candidate {
  290.                 final Ref tag;
  291.                 final RevFlag flag;

  292.                 /**
  293.                  * This field counts number of commits that are reachable from
  294.                  * the tip but not reachable from the tag.
  295.                  */
  296.                 int depth;

  297.                 Candidate(RevCommit commit, Ref tag) {
  298.                     this.tag = tag;
  299.                     this.flag = w.newFlag(tag.getName());
  300.                     // we'll mark all the nodes reachable from this tag accordingly
  301.                     allFlags.add(flag);
  302.                     w.carry(flag);
  303.                     commit.add(flag);
  304.                     // As of this writing, JGit carries a flag from a child to its parents
  305.                     // right before RevWalk.next() returns, so all the flags that are added
  306.                     // must be manually carried to its parents. If that gets fixed,
  307.                     // this will be unnecessary.
  308.                     commit.carry(flag);
  309.                 }

  310.                 /**
  311.                  * Does this tag contain the given commit?
  312.                  */
  313.                 boolean reaches(RevCommit c) {
  314.                     return c.has(flag);
  315.                 }

  316.                 String describe(ObjectId tip) throws IOException {
  317.                     return longDescription(tag, depth, tip);
  318.                 }

  319.             }
  320.             List<Candidate> candidates = new ArrayList<>();    // all the candidates we find

  321.             // is the target already pointing to a suitable tag? if so, we are done!
  322.             Optional<Ref> bestMatch = getBestMatch(tags.get(target));
  323.             if (bestMatch.isPresent()) {
  324.                 return longDesc ? longDescription(bestMatch.get(), 0, target) :
  325.                         formatRefName(bestMatch.get().getName());
  326.             }

  327.             w.markStart(target);

  328.             int seen = 0;   // commit seen thus far
  329.             RevCommit c;
  330.             while ((c = w.next()) != null) {
  331.                 if (!c.hasAny(allFlags)) {
  332.                     // if a tag already dominates this commit,
  333.                     // then there's no point in picking a tag on this commit
  334.                     // since the one that dominates it is always more preferable
  335.                     bestMatch = getBestMatch(tags.get(c));
  336.                     if (bestMatch.isPresent()) {
  337.                         Candidate cd = new Candidate(c, bestMatch.get());
  338.                         candidates.add(cd);
  339.                         cd.depth = seen;
  340.                     }
  341.                 }

  342.                 // if the newly discovered commit isn't reachable from a tag that we've seen
  343.                 // it counts toward the total depth.
  344.                 for (Candidate cd : candidates) {
  345.                     if (!cd.reaches(c))
  346.                         cd.depth++;
  347.                 }

  348.                 // if we have search going for enough tags, we will start
  349.                 // closing down. JGit can only give us a finite number of bits,
  350.                 // so we can't track all tags even if we wanted to.
  351.                 if (candidates.size() >= maxCandidates)
  352.                     break;

  353.                 // TODO: if all the commits in the queue of RevWalk has allFlags
  354.                 // there's no point in continuing search as we'll not discover any more
  355.                 // tags. But RevWalk doesn't expose this.
  356.                 seen++;
  357.             }

  358.             // at this point we aren't adding any more tags to our search,
  359.             // but we still need to count all the depths correctly.
  360.             while ((c = w.next()) != null) {
  361.                 if (c.hasAll(allFlags)) {
  362.                     // no point in visiting further from here, so cut the search here
  363.                     for (RevCommit p : c.getParents())
  364.                         p.add(RevFlag.SEEN);
  365.                 } else {
  366.                     for (Candidate cd : candidates) {
  367.                         if (!cd.reaches(c))
  368.                             cd.depth++;
  369.                     }
  370.                 }
  371.             }

  372.             // if all the nodes are dominated by all the tags, the walk stops
  373.             if (candidates.isEmpty()) {
  374.                 return always ? w.getObjectReader().abbreviate(target).name() : null;
  375.             }

  376.             Candidate best = Collections.min(candidates,
  377.                     (Candidate o1, Candidate o2) -> o1.depth - o2.depth);

  378.             return best.describe(target);
  379.         } catch (IOException e) {
  380.             throw new JGitInternalException(e.getMessage(), e);
  381.         } finally {
  382.             setCallable(false);
  383.             w.close();
  384.         }
  385.     }

  386.     /**
  387.      * Removes the refs/ or refs/tags prefix from tag names
  388.      * @param name the name of the tag
  389.      * @return the tag name with its prefix removed
  390.      */
  391.     private String formatRefName(String name) {
  392.         return name.startsWith(R_TAGS) ? name.substring(R_TAGS.length()) :
  393.                 name.substring(R_REFS.length());
  394.     }

  395.     /**
  396.      * Whether we use lightweight tags or not for describe Candidates
  397.      *
  398.      * @param ref
  399.      *            reference under inspection
  400.      * @return true if it should be used for describe or not regarding
  401.      *         {@link org.eclipse.jgit.api.DescribeCommand#useTags}
  402.      */
  403.     @SuppressWarnings("null")
  404.     private boolean filterLightweightTags(Ref ref) {
  405.         ObjectId id = ref.getObjectId();
  406.         try {
  407.             return this.useAll || this.useTags || (id != null && (w.parseTag(id) != null));
  408.         } catch (IOException e) {
  409.             return false;
  410.         }
  411.     }
  412. }