NameRevCommand.java

  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. package org.eclipse.jgit.api;

  11. import java.io.IOException;
  12. import java.util.ArrayList;
  13. import java.util.HashMap;
  14. import java.util.LinkedHashMap;
  15. import java.util.List;
  16. import java.util.Map;

  17. import org.eclipse.jgit.api.errors.GitAPIException;
  18. import org.eclipse.jgit.api.errors.JGitInternalException;
  19. import org.eclipse.jgit.errors.MissingObjectException;
  20. import org.eclipse.jgit.lib.AnyObjectId;
  21. import org.eclipse.jgit.lib.Constants;
  22. import org.eclipse.jgit.lib.ObjectId;
  23. import org.eclipse.jgit.lib.Ref;
  24. import org.eclipse.jgit.lib.Repository;
  25. import org.eclipse.jgit.revwalk.FIFORevQueue;
  26. import org.eclipse.jgit.revwalk.RevCommit;
  27. import org.eclipse.jgit.revwalk.RevObject;
  28. import org.eclipse.jgit.revwalk.RevTag;
  29. import org.eclipse.jgit.revwalk.RevWalk;

  30. /**
  31.  * Command to find human-readable names of revisions.
  32.  *
  33.  * @see <a
  34.  *      href="http://www.kernel.org/pub/software/scm/git/docs/git-name-rev.html"
  35.  *      >Git documentation about name-rev</a>
  36.  * @since 3.0
  37.  */
  38. public class NameRevCommand extends GitCommand<Map<ObjectId, String>> {
  39.     /** Amount of slop to allow walking past the earliest requested commit. */
  40.     private static final int COMMIT_TIME_SLOP = 60 * 60 * 24;

  41.     /** Cost of traversing a merge commit compared to a linear history. */
  42.     private static final int MERGE_COST = 65535;

  43.     private static class NameRevCommit extends RevCommit {
  44.         private String tip;
  45.         private int distance;
  46.         private long cost;

  47.         private NameRevCommit(AnyObjectId id) {
  48.             super(id);
  49.         }

  50.         private StringBuilder format() {
  51.             StringBuilder sb = new StringBuilder(tip);
  52.             if (distance > 0)
  53.                 sb.append('~').append(distance);
  54.             return sb;
  55.         }

  56.         @Override
  57.         public String toString() {
  58.             StringBuilder sb = new StringBuilder(getClass().getSimpleName())
  59.                 .append('[');
  60.             if (tip != null) {
  61.                 sb.append(format());
  62.             } else {
  63.                 sb.append((Object) null);
  64.             }
  65.             sb.append(',').append(cost).append(']').append(' ')
  66.                     .append(super.toString());
  67.             return sb.toString();
  68.         }
  69.     }

  70.     private final RevWalk walk;
  71.     private final List<String> prefixes;
  72.     private final List<ObjectId> revs;
  73.     private List<Ref> refs;
  74.     private int mergeCost;

  75.     /**
  76.      * Create a new name-rev command.
  77.      *
  78.      * @param repo
  79.      *            the {@link org.eclipse.jgit.lib.Repository}
  80.      */
  81.     protected NameRevCommand(Repository repo) {
  82.         super(repo);
  83.         mergeCost = MERGE_COST;
  84.         prefixes = new ArrayList<>(2);
  85.         revs = new ArrayList<>(2);
  86.         walk = new RevWalk(repo) {
  87.             @Override
  88.             public NameRevCommit createCommit(AnyObjectId id) {
  89.                 return new NameRevCommit(id);
  90.             }
  91.         };
  92.     }

  93.     /** {@inheritDoc} */
  94.     @Override
  95.     public Map<ObjectId, String> call() throws GitAPIException {
  96.         try {
  97.             Map<ObjectId, String> nonCommits = new HashMap<>();
  98.             FIFORevQueue pending = new FIFORevQueue();
  99.             if (refs != null) {
  100.                 for (Ref ref : refs)
  101.                     addRef(ref, nonCommits, pending);
  102.             }
  103.             addPrefixes(nonCommits, pending);
  104.             int cutoff = minCommitTime() - COMMIT_TIME_SLOP;

  105.             while (true) {
  106.                 NameRevCommit c = (NameRevCommit) pending.next();
  107.                 if (c == null)
  108.                     break;
  109.                 if (c.getCommitTime() < cutoff)
  110.                     continue;
  111.                 for (int i = 0; i < c.getParentCount(); i++) {
  112.                     NameRevCommit p = (NameRevCommit) walk.parseCommit(c.getParent(i));
  113.                     long cost = c.cost + (i > 0 ? mergeCost : 1);
  114.                     if (p.tip == null || compare(c.tip, cost, p.tip, p.cost) < 0) {
  115.                         if (i > 0) {
  116.                             p.tip = c.format().append('^').append(i + 1).toString();
  117.                             p.distance = 0;
  118.                         } else {
  119.                             p.tip = c.tip;
  120.                             p.distance = c.distance + 1;
  121.                         }
  122.                         p.cost = cost;
  123.                         pending.add(p);
  124.                     }
  125.                 }
  126.             }

  127.             Map<ObjectId, String> result =
  128.                 new LinkedHashMap<>(revs.size());
  129.             for (ObjectId id : revs) {
  130.                 RevObject o = walk.parseAny(id);
  131.                 if (o instanceof NameRevCommit) {
  132.                     NameRevCommit c = (NameRevCommit) o;
  133.                     if (c.tip != null)
  134.                         result.put(id, simplify(c.format().toString()));
  135.                 } else {
  136.                     String name = nonCommits.get(id);
  137.                     if (name != null)
  138.                         result.put(id, simplify(name));
  139.                 }
  140.             }

  141.             setCallable(false);
  142.             return result;
  143.         } catch (IOException e) {
  144.             throw new JGitInternalException(e.getMessage(), e);
  145.         } finally {
  146.             walk.close();
  147.         }
  148.     }

  149.     /**
  150.      * Add an object to search for.
  151.      *
  152.      * @param id
  153.      *            object ID to add.
  154.      * @return {@code this}
  155.      * @throws org.eclipse.jgit.errors.MissingObjectException
  156.      *             the object supplied is not available from the object
  157.      *             database.
  158.      * @throws org.eclipse.jgit.api.errors.JGitInternalException
  159.      *             a low-level exception of JGit has occurred. The original
  160.      *             exception can be retrieved by calling
  161.      *             {@link java.lang.Exception#getCause()}.
  162.      */
  163.     public NameRevCommand add(ObjectId id) throws MissingObjectException,
  164.             JGitInternalException {
  165.         checkCallable();
  166.         try {
  167.             walk.parseAny(id);
  168.         } catch (MissingObjectException e) {
  169.             throw e;
  170.         } catch (IOException e) {
  171.             throw new JGitInternalException(e.getMessage(), e);
  172.         }
  173.         revs.add(id.copy());
  174.         return this;
  175.     }

  176.     /**
  177.      * Add multiple objects to search for.
  178.      *
  179.      * @param ids
  180.      *            object IDs to add.
  181.      * @return {@code this}
  182.      * @throws org.eclipse.jgit.errors.MissingObjectException
  183.      *             the object supplied is not available from the object
  184.      *             database.
  185.      * @throws org.eclipse.jgit.api.errors.JGitInternalException
  186.      *             a low-level exception of JGit has occurred. The original
  187.      *             exception can be retrieved by calling
  188.      *             {@link java.lang.Exception#getCause()}.
  189.      */
  190.     public NameRevCommand add(Iterable<ObjectId> ids)
  191.             throws MissingObjectException, JGitInternalException {
  192.         for (ObjectId id : ids)
  193.             add(id);
  194.         return this;
  195.     }

  196.     /**
  197.      * Add a ref prefix to the set that results must match.
  198.      * <p>
  199.      * If an object matches multiple refs equally well, the first matching ref
  200.      * added with {@link #addRef(Ref)} is preferred, or else the first matching
  201.      * prefix added by {@link #addPrefix(String)}.
  202.      *
  203.      * @param prefix
  204.      *            prefix to add; the prefix must end with a slash
  205.      * @return {@code this}
  206.      */
  207.     public NameRevCommand addPrefix(String prefix) {
  208.         checkCallable();
  209.         prefixes.add(prefix);
  210.         return this;
  211.     }

  212.     /**
  213.      * Add all annotated tags under {@code refs/tags/} to the set that all
  214.      * results must match.
  215.      * <p>
  216.      * Calls {@link #addRef(Ref)}; see that method for a note on matching
  217.      * priority.
  218.      *
  219.      * @return {@code this}
  220.      * @throws JGitInternalException
  221.      *             a low-level exception of JGit has occurred. The original
  222.      *             exception can be retrieved by calling
  223.      *             {@link java.lang.Exception#getCause()}.
  224.      */
  225.     public NameRevCommand addAnnotatedTags() {
  226.         checkCallable();
  227.         if (refs == null)
  228.             refs = new ArrayList<>();
  229.         try {
  230.             for (Ref ref : repo.getRefDatabase()
  231.                     .getRefsByPrefix(Constants.R_TAGS)) {
  232.                 ObjectId id = ref.getObjectId();
  233.                 if (id != null && (walk.parseAny(id) instanceof RevTag))
  234.                     addRef(ref);
  235.             }
  236.         } catch (IOException e) {
  237.             throw new JGitInternalException(e.getMessage(), e);
  238.         }
  239.         return this;
  240.     }

  241.     /**
  242.      * Add a ref to the set that all results must match.
  243.      * <p>
  244.      * If an object matches multiple refs equally well, the first matching ref
  245.      * added with {@link #addRef(Ref)} is preferred, or else the first matching
  246.      * prefix added by {@link #addPrefix(String)}.
  247.      *
  248.      * @param ref
  249.      *            ref to add.
  250.      * @return {@code this}
  251.      */
  252.     public NameRevCommand addRef(Ref ref) {
  253.         checkCallable();
  254.         if (refs == null)
  255.             refs = new ArrayList<>();
  256.         refs.add(ref);
  257.         return this;
  258.     }

  259.     NameRevCommand setMergeCost(int cost) {
  260.         mergeCost = cost;
  261.         return this;
  262.     }

  263.     private void addPrefixes(Map<ObjectId, String> nonCommits,
  264.             FIFORevQueue pending) throws IOException {
  265.         if (!prefixes.isEmpty()) {
  266.             for (String prefix : prefixes)
  267.                 addPrefix(prefix, nonCommits, pending);
  268.         } else if (refs == null)
  269.             addPrefix(Constants.R_REFS, nonCommits, pending);
  270.     }

  271.     private void addPrefix(String prefix, Map<ObjectId, String> nonCommits,
  272.             FIFORevQueue pending) throws IOException {
  273.         for (Ref ref : repo.getRefDatabase().getRefsByPrefix(prefix))
  274.             addRef(ref, nonCommits, pending);
  275.     }

  276.     private void addRef(Ref ref, Map<ObjectId, String> nonCommits,
  277.             FIFORevQueue pending) throws IOException {
  278.         if (ref.getObjectId() == null)
  279.             return;
  280.         RevObject o = walk.parseAny(ref.getObjectId());
  281.         while (o instanceof RevTag) {
  282.             RevTag t = (RevTag) o;
  283.             nonCommits.put(o, ref.getName());
  284.             o = t.getObject();
  285.             walk.parseHeaders(o);
  286.         }
  287.         if (o instanceof NameRevCommit) {
  288.             NameRevCommit c = (NameRevCommit) o;
  289.             if (c.tip == null)
  290.                 c.tip = ref.getName();
  291.             pending.add(c);
  292.         } else if (!nonCommits.containsKey(o))
  293.             nonCommits.put(o, ref.getName());
  294.     }

  295.     private int minCommitTime() throws IOException {
  296.         int min = Integer.MAX_VALUE;
  297.         for (ObjectId id : revs) {
  298.             RevObject o = walk.parseAny(id);
  299.             while (o instanceof RevTag) {
  300.                 o = ((RevTag) o).getObject();
  301.                 walk.parseHeaders(o);
  302.             }
  303.             if (o instanceof RevCommit) {
  304.                 RevCommit c = (RevCommit) o;
  305.                 if (c.getCommitTime() < min)
  306.                     min = c.getCommitTime();
  307.             }
  308.         }
  309.         return min;
  310.     }

  311.     private long compare(String leftTip, long leftCost, String rightTip, long rightCost) {
  312.         long c = leftCost - rightCost;
  313.         if (c != 0 || prefixes.isEmpty())
  314.             return c;
  315.         int li = -1;
  316.         int ri = -1;
  317.         for (int i = 0; i < prefixes.size(); i++) {
  318.             String prefix = prefixes.get(i);
  319.             if (li < 0 && leftTip.startsWith(prefix))
  320.                 li = i;
  321.             if (ri < 0 && rightTip.startsWith(prefix))
  322.                 ri = i;
  323.         }
  324.         // Don't tiebreak if prefixes are the same, in order to prefer first-parent
  325.         // paths.
  326.         return li - ri;
  327.     }

  328.     private static String simplify(String refName) {
  329.         if (refName.startsWith(Constants.R_HEADS))
  330.             return refName.substring(Constants.R_HEADS.length());
  331.         if (refName.startsWith(Constants.R_TAGS))
  332.             return refName.substring(Constants.R_TAGS.length());
  333.         if (refName.startsWith(Constants.R_REFS))
  334.             return refName.substring(Constants.R_REFS.length());
  335.         return refName;
  336.     }
  337. }