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.             sb.append(',').append(cost).append(']').append(' ')
  65.                 .append(super.toString()).toString();
  66.             return sb.toString();
  67.         }
  68.     }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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