RenameDetector.java

  1. /*
  2.  * Copyright (C) 2010, 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.diff;

  11. import static org.eclipse.jgit.diff.DiffEntry.Side.NEW;
  12. import static org.eclipse.jgit.diff.DiffEntry.Side.OLD;

  13. import java.io.IOException;
  14. import java.util.ArrayList;
  15. import java.util.Arrays;
  16. import java.util.Collection;
  17. import java.util.Collections;
  18. import java.util.Comparator;
  19. import java.util.HashMap;
  20. import java.util.List;

  21. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  22. import org.eclipse.jgit.diff.SimilarityIndex.TableFullException;
  23. import org.eclipse.jgit.errors.CancelledException;
  24. import org.eclipse.jgit.internal.JGitText;
  25. import org.eclipse.jgit.lib.AbbreviatedObjectId;
  26. import org.eclipse.jgit.lib.FileMode;
  27. import org.eclipse.jgit.lib.NullProgressMonitor;
  28. import org.eclipse.jgit.lib.ObjectReader;
  29. import org.eclipse.jgit.lib.ProgressMonitor;
  30. import org.eclipse.jgit.lib.Repository;

  31. /**
  32.  * Detect and resolve object renames.
  33.  */
  34. public class RenameDetector {
  35.     private static final int EXACT_RENAME_SCORE = 100;

  36.     private static final Comparator<DiffEntry> DIFF_COMPARATOR = new Comparator<DiffEntry>() {
  37.         @Override
  38.         public int compare(DiffEntry a, DiffEntry b) {
  39.             int cmp = nameOf(a).compareTo(nameOf(b));
  40.             if (cmp == 0)
  41.                 cmp = sortOf(a.getChangeType()) - sortOf(b.getChangeType());
  42.             return cmp;
  43.         }

  44.         private String nameOf(DiffEntry ent) {
  45.             // Sort by the new name, unless the change is a delete. On
  46.             // deletes the new name is /dev/null, so we sort instead by
  47.             // the old name.
  48.             //
  49.             if (ent.changeType == ChangeType.DELETE)
  50.                 return ent.oldPath;
  51.             return ent.newPath;
  52.         }

  53.         private int sortOf(ChangeType changeType) {
  54.             // Sort deletes before adds so that a major type change for
  55.             // a file path (such as symlink to regular file) will first
  56.             // remove the path, then add it back with the new type.
  57.             //
  58.             switch (changeType) {
  59.             case DELETE:
  60.                 return 1;
  61.             case ADD:
  62.                 return 2;
  63.             default:
  64.                 return 10;
  65.             }
  66.         }
  67.     };

  68.     private List<DiffEntry> entries;

  69.     private List<DiffEntry> deleted;

  70.     private List<DiffEntry> added;

  71.     private boolean done;

  72.     private final ObjectReader objectReader;

  73.     /** Similarity score required to pair an add/delete as a rename. */
  74.     private int renameScore = 60;

  75.     /**
  76.      * Similarity score required to keep modified file pairs together. Any
  77.      * modified file pairs with a similarity score below this will be broken
  78.      * apart.
  79.      */
  80.     private int breakScore = -1;

  81.     /** Limit in the number of files to consider for renames. */
  82.     private int renameLimit;

  83.     /** Set if the number of adds or deletes was over the limit. */
  84.     private boolean overRenameLimit;

  85.     /**
  86.      * Create a new rename detector for the given repository
  87.      *
  88.      * @param repo
  89.      *            the repository to use for rename detection
  90.      */
  91.     public RenameDetector(Repository repo) {
  92.         this(repo.newObjectReader(), repo.getConfig().get(DiffConfig.KEY));
  93.     }

  94.     /**
  95.      * Create a new rename detector with a specified reader and diff config.
  96.      *
  97.      * @param reader
  98.      *            reader to obtain objects from the repository with.
  99.      * @param cfg
  100.      *            diff config specifying rename detection options.
  101.      * @since 3.0
  102.      */
  103.     public RenameDetector(ObjectReader reader, DiffConfig cfg) {
  104.         objectReader = reader.newReader();
  105.         renameLimit = cfg.getRenameLimit();
  106.         reset();
  107.     }

  108.     /**
  109.      * Get rename score
  110.      *
  111.      * @return minimum score required to pair an add/delete as a rename. The
  112.      *         score ranges are within the bounds of (0, 100).
  113.      */
  114.     public int getRenameScore() {
  115.         return renameScore;
  116.     }

  117.     /**
  118.      * Set the minimum score required to pair an add/delete as a rename.
  119.      * <p>
  120.      * When comparing two files together their score must be greater than or
  121.      * equal to the rename score for them to be considered a rename match. The
  122.      * score is computed based on content similarity, so a score of 60 implies
  123.      * that approximately 60% of the bytes in the files are identical.
  124.      *
  125.      * @param score
  126.      *            new rename score, must be within [0, 100].
  127.      * @throws java.lang.IllegalArgumentException
  128.      *             the score was not within [0, 100].
  129.      */
  130.     public void setRenameScore(int score) {
  131.         if (score < 0 || score > 100)
  132.             throw new IllegalArgumentException(
  133.                     JGitText.get().similarityScoreMustBeWithinBounds);
  134.         renameScore = score;
  135.     }

  136.     /**
  137.      * Get break score
  138.      *
  139.      * @return the similarity score required to keep modified file pairs
  140.      *         together. Any modify pairs that score below this will be broken
  141.      *         apart into separate add/deletes. Values less than or equal to
  142.      *         zero indicate that no modifies will be broken apart. Values over
  143.      *         100 cause all modify pairs to be broken.
  144.      */
  145.     public int getBreakScore() {
  146.         return breakScore;
  147.     }

  148.     /**
  149.      * Set break score
  150.      *
  151.      * @param breakScore
  152.      *            the similarity score required to keep modified file pairs
  153.      *            together. Any modify pairs that score below this will be
  154.      *            broken apart into separate add/deletes. Values less than or
  155.      *            equal to zero indicate that no modifies will be broken apart.
  156.      *            Values over 100 cause all modify pairs to be broken.
  157.      */
  158.     public void setBreakScore(int breakScore) {
  159.         this.breakScore = breakScore;
  160.     }

  161.     /**
  162.      * Get rename limit
  163.      *
  164.      * @return limit on number of paths to perform inexact rename detection
  165.      */
  166.     public int getRenameLimit() {
  167.         return renameLimit;
  168.     }

  169.     /**
  170.      * Set the limit on the number of files to perform inexact rename detection.
  171.      * <p>
  172.      * The rename detector has to build a square matrix of the rename limit on
  173.      * each side, then perform that many file compares to determine similarity.
  174.      * If 1000 files are added, and 1000 files are deleted, a 1000*1000 matrix
  175.      * must be allocated, and 1,000,000 file compares may need to be performed.
  176.      *
  177.      * @param limit
  178.      *            new file limit. 0 means no limit; a negative number means no
  179.      *            inexact rename detection will be performed, only exact rename
  180.      *            detection.
  181.      */
  182.     public void setRenameLimit(int limit) {
  183.         renameLimit = limit;
  184.     }

  185.     /**
  186.      * Check if the detector is over the rename limit.
  187.      * <p>
  188.      * This method can be invoked either before or after {@code getEntries} has
  189.      * been used to perform rename detection.
  190.      *
  191.      * @return true if the detector has more file additions or removals than the
  192.      *         rename limit is currently set to. In such configurations the
  193.      *         detector will skip expensive computation.
  194.      */
  195.     public boolean isOverRenameLimit() {
  196.         if (done)
  197.             return overRenameLimit;
  198.         int cnt = Math.max(added.size(), deleted.size());
  199.         return getRenameLimit() != 0 && getRenameLimit() < cnt;
  200.     }

  201.     /**
  202.      * Add entries to be considered for rename detection.
  203.      *
  204.      * @param entriesToAdd
  205.      *            one or more entries to add.
  206.      * @throws java.lang.IllegalStateException
  207.      *             if {@code getEntries} was already invoked.
  208.      */
  209.     public void addAll(Collection<DiffEntry> entriesToAdd) {
  210.         if (done)
  211.             throw new IllegalStateException(JGitText.get().renamesAlreadyFound);

  212.         for (DiffEntry entry : entriesToAdd) {
  213.             switch (entry.getChangeType()) {
  214.             case ADD:
  215.                 added.add(entry);
  216.                 break;

  217.             case DELETE:
  218.                 deleted.add(entry);
  219.                 break;

  220.             case MODIFY:
  221.                 if (sameType(entry.getOldMode(), entry.getNewMode())) {
  222.                     entries.add(entry);
  223.                 } else {
  224.                     List<DiffEntry> tmp = DiffEntry.breakModify(entry);
  225.                     deleted.add(tmp.get(0));
  226.                     added.add(tmp.get(1));
  227.                 }
  228.                 break;

  229.             case COPY:
  230.             case RENAME:
  231.             default:
  232.                 entries.add(entry);
  233.             }
  234.         }
  235.     }

  236.     /**
  237.      * Add an entry to be considered for rename detection.
  238.      *
  239.      * @param entry
  240.      *            to add.
  241.      * @throws java.lang.IllegalStateException
  242.      *             if {@code getEntries} was already invoked.
  243.      */
  244.     public void add(DiffEntry entry) {
  245.         addAll(Collections.singletonList(entry));
  246.     }

  247.     /**
  248.      * Detect renames in the current file set.
  249.      * <p>
  250.      * This convenience function runs without a progress monitor.
  251.      *
  252.      * @return an unmodifiable list of {@link org.eclipse.jgit.diff.DiffEntry}s
  253.      *         representing all files that have been changed.
  254.      * @throws java.io.IOException
  255.      *             file contents cannot be read from the repository.
  256.      */
  257.     public List<DiffEntry> compute() throws IOException {
  258.         return compute(NullProgressMonitor.INSTANCE);
  259.     }

  260.     /**
  261.      * Detect renames in the current file set.
  262.      *
  263.      * @param pm
  264.      *            report progress during the detection phases.
  265.      * @return an unmodifiable list of {@link org.eclipse.jgit.diff.DiffEntry}s
  266.      *         representing all files that have been changed.
  267.      * @throws java.io.IOException
  268.      *             file contents cannot be read from the repository.
  269.      * @throws CancelledException
  270.      *             if rename detection was cancelled
  271.      */
  272.     // TODO(ms): use org.eclipse.jgit.api.errors.CanceledException in next major
  273.     // version
  274.     public List<DiffEntry> compute(ProgressMonitor pm)
  275.             throws IOException, CancelledException {
  276.         if (!done) {
  277.             try {
  278.                 return compute(objectReader, pm);
  279.             } finally {
  280.                 objectReader.close();
  281.             }
  282.         }
  283.         return Collections.unmodifiableList(entries);
  284.     }

  285.     /**
  286.      * Detect renames in the current file set.
  287.      *
  288.      * @param reader
  289.      *            reader to obtain objects from the repository with.
  290.      * @param pm
  291.      *            report progress during the detection phases.
  292.      * @return an unmodifiable list of {@link org.eclipse.jgit.diff.DiffEntry}s
  293.      *         representing all files that have been changed.
  294.      * @throws java.io.IOException
  295.      *             file contents cannot be read from the repository.
  296.      * @throws CancelledException
  297.      *             if rename detection was cancelled
  298.      */
  299.     // TODO(ms): use org.eclipse.jgit.api.errors.CanceledException in next major
  300.     // version
  301.     public List<DiffEntry> compute(ObjectReader reader, ProgressMonitor pm)
  302.             throws IOException, CancelledException {
  303.         final ContentSource cs = ContentSource.create(reader);
  304.         return compute(new ContentSource.Pair(cs, cs), pm);
  305.     }

  306.     /**
  307.      * Detect renames in the current file set.
  308.      *
  309.      * @param reader
  310.      *            reader to obtain objects from the repository with.
  311.      * @param pm
  312.      *            report progress during the detection phases.
  313.      * @return an unmodifiable list of {@link org.eclipse.jgit.diff.DiffEntry}s
  314.      *         representing all files that have been changed.
  315.      * @throws java.io.IOException
  316.      *             file contents cannot be read from the repository.
  317.      * @throws CancelledException
  318.      *             if rename detection was cancelled
  319.      */
  320.     // TODO(ms): use org.eclipse.jgit.api.errors.CanceledException in next major
  321.     // version
  322.     public List<DiffEntry> compute(ContentSource.Pair reader, ProgressMonitor pm)
  323.             throws IOException, CancelledException {
  324.         if (!done) {
  325.             done = true;

  326.             if (pm == null)
  327.                 pm = NullProgressMonitor.INSTANCE;

  328.             if (0 < breakScore)
  329.                 breakModifies(reader, pm);

  330.             if (!added.isEmpty() && !deleted.isEmpty())
  331.                 findExactRenames(pm);

  332.             if (!added.isEmpty() && !deleted.isEmpty())
  333.                 findContentRenames(reader, pm);

  334.             if (0 < breakScore && !added.isEmpty() && !deleted.isEmpty())
  335.                 rejoinModifies(pm);

  336.             entries.addAll(added);
  337.             added = null;

  338.             entries.addAll(deleted);
  339.             deleted = null;

  340.             Collections.sort(entries, DIFF_COMPARATOR);
  341.         }
  342.         return Collections.unmodifiableList(entries);
  343.     }

  344.     /**
  345.      * Reset this rename detector for another rename detection pass.
  346.      */
  347.     public void reset() {
  348.         entries = new ArrayList<>();
  349.         deleted = new ArrayList<>();
  350.         added = new ArrayList<>();
  351.         done = false;
  352.     }

  353.     private void advanceOrCancel(ProgressMonitor pm) throws CancelledException {
  354.         if (pm.isCancelled()) {
  355.             throw new CancelledException(JGitText.get().renameCancelled);
  356.         }
  357.         pm.update(1);
  358.     }

  359.     private void breakModifies(ContentSource.Pair reader, ProgressMonitor pm)
  360.             throws IOException, CancelledException {
  361.         ArrayList<DiffEntry> newEntries = new ArrayList<>(entries.size());

  362.         pm.beginTask(JGitText.get().renamesBreakingModifies, entries.size());

  363.         for (int i = 0; i < entries.size(); i++) {
  364.             DiffEntry e = entries.get(i);
  365.             if (e.getChangeType() == ChangeType.MODIFY) {
  366.                 int score = calculateModifyScore(reader, e);
  367.                 if (score < breakScore) {
  368.                     List<DiffEntry> tmp = DiffEntry.breakModify(e);
  369.                     DiffEntry del = tmp.get(0);
  370.                     del.score = score;
  371.                     deleted.add(del);
  372.                     added.add(tmp.get(1));
  373.                 } else {
  374.                     newEntries.add(e);
  375.                 }
  376.             } else {
  377.                 newEntries.add(e);
  378.             }
  379.             advanceOrCancel(pm);
  380.         }

  381.         entries = newEntries;
  382.     }

  383.     private void rejoinModifies(ProgressMonitor pm) throws CancelledException {
  384.         HashMap<String, DiffEntry> nameMap = new HashMap<>();
  385.         ArrayList<DiffEntry> newAdded = new ArrayList<>(added.size());

  386.         pm.beginTask(JGitText.get().renamesRejoiningModifies, added.size()
  387.                 + deleted.size());

  388.         for (DiffEntry src : deleted) {
  389.             nameMap.put(src.oldPath, src);
  390.             advanceOrCancel(pm);
  391.         }

  392.         for (DiffEntry dst : added) {
  393.             DiffEntry src = nameMap.remove(dst.newPath);
  394.             if (src != null) {
  395.                 if (sameType(src.oldMode, dst.newMode)) {
  396.                     entries.add(DiffEntry.pair(ChangeType.MODIFY, src, dst,
  397.                             src.score));
  398.                 } else {
  399.                     nameMap.put(src.oldPath, src);
  400.                     newAdded.add(dst);
  401.                 }
  402.             } else {
  403.                 newAdded.add(dst);
  404.             }
  405.             advanceOrCancel(pm);
  406.         }

  407.         added = newAdded;
  408.         deleted = new ArrayList<>(nameMap.values());
  409.     }

  410.     private int calculateModifyScore(ContentSource.Pair reader, DiffEntry d)
  411.             throws IOException {
  412.         try {
  413.             SimilarityIndex src = new SimilarityIndex();
  414.             src.hash(reader.open(OLD, d));
  415.             src.sort();

  416.             SimilarityIndex dst = new SimilarityIndex();
  417.             dst.hash(reader.open(NEW, d));
  418.             dst.sort();
  419.             return src.score(dst, 100);
  420.         } catch (TableFullException tableFull) {
  421.             // If either table overflowed while being constructed, don't allow
  422.             // the pair to be broken. Returning 1 higher than breakScore will
  423.             // ensure its not similar, but not quite dissimilar enough to break.
  424.             //
  425.             overRenameLimit = true;
  426.             return breakScore + 1;
  427.         }
  428.     }

  429.     private void findContentRenames(ContentSource.Pair reader,
  430.             ProgressMonitor pm)
  431.             throws IOException, CancelledException {
  432.         int cnt = Math.max(added.size(), deleted.size());
  433.         if (getRenameLimit() == 0 || cnt <= getRenameLimit()) {
  434.             SimilarityRenameDetector d;

  435.             d = new SimilarityRenameDetector(reader, deleted, added);
  436.             d.setRenameScore(getRenameScore());
  437.             d.compute(pm);
  438.             overRenameLimit |= d.isTableOverflow();
  439.             deleted = d.getLeftOverSources();
  440.             added = d.getLeftOverDestinations();
  441.             entries.addAll(d.getMatches());
  442.         } else {
  443.             overRenameLimit = true;
  444.         }
  445.     }

  446.     @SuppressWarnings("unchecked")
  447.     private void findExactRenames(ProgressMonitor pm)
  448.             throws CancelledException {
  449.         pm.beginTask(JGitText.get().renamesFindingExact, //
  450.                 added.size() + added.size() + deleted.size()
  451.                         + added.size() * deleted.size());

  452.         HashMap<AbbreviatedObjectId, Object> deletedMap = populateMap(deleted, pm);
  453.         HashMap<AbbreviatedObjectId, Object> addedMap = populateMap(added, pm);

  454.         ArrayList<DiffEntry> uniqueAdds = new ArrayList<>(added.size());
  455.         ArrayList<List<DiffEntry>> nonUniqueAdds = new ArrayList<>();

  456.         for (Object o : addedMap.values()) {
  457.             if (o instanceof DiffEntry)
  458.                 uniqueAdds.add((DiffEntry) o);
  459.             else
  460.                 nonUniqueAdds.add((List<DiffEntry>) o);
  461.         }

  462.         ArrayList<DiffEntry> left = new ArrayList<>(added.size());

  463.         for (DiffEntry a : uniqueAdds) {
  464.             Object del = deletedMap.get(a.newId);
  465.             if (del instanceof DiffEntry) {
  466.                 // We have one add to one delete: pair them if they are the same
  467.                 // type
  468.                 DiffEntry e = (DiffEntry) del;
  469.                 if (sameType(e.oldMode, a.newMode)) {
  470.                     e.changeType = ChangeType.RENAME;
  471.                     entries.add(exactRename(e, a));
  472.                 } else {
  473.                     left.add(a);
  474.                 }
  475.             } else if (del != null) {
  476.                 // We have one add to many deletes: find the delete with the
  477.                 // same type and closest name to the add, then pair them
  478.                 List<DiffEntry> list = (List<DiffEntry>) del;
  479.                 DiffEntry best = bestPathMatch(a, list);
  480.                 if (best != null) {
  481.                     best.changeType = ChangeType.RENAME;
  482.                     entries.add(exactRename(best, a));
  483.                 } else {
  484.                     left.add(a);
  485.                 }
  486.             } else {
  487.                 left.add(a);
  488.             }
  489.             advanceOrCancel(pm);
  490.         }

  491.         for (List<DiffEntry> adds : nonUniqueAdds) {
  492.             Object o = deletedMap.get(adds.get(0).newId);
  493.             if (o instanceof DiffEntry) {
  494.                 // We have many adds to one delete: find the add with the same
  495.                 // type and closest name to the delete, then pair them. Mark the
  496.                 // rest as copies of the delete.
  497.                 DiffEntry d = (DiffEntry) o;
  498.                 DiffEntry best = bestPathMatch(d, adds);
  499.                 if (best != null) {
  500.                     d.changeType = ChangeType.RENAME;
  501.                     entries.add(exactRename(d, best));
  502.                     for (DiffEntry a : adds) {
  503.                         if (a != best) {
  504.                             if (sameType(d.oldMode, a.newMode)) {
  505.                                 entries.add(exactCopy(d, a));
  506.                             } else {
  507.                                 left.add(a);
  508.                             }
  509.                         }
  510.                     }
  511.                 } else {
  512.                     left.addAll(adds);
  513.                 }
  514.             } else if (o != null) {
  515.                 // We have many adds to many deletes: score all the adds against
  516.                 // all the deletes by path name, take the best matches, pair
  517.                 // them as renames, then call the rest copies
  518.                 List<DiffEntry> dels = (List<DiffEntry>) o;
  519.                 long[] matrix = new long[dels.size() * adds.size()];
  520.                 int mNext = 0;
  521.                 for (int delIdx = 0; delIdx < dels.size(); delIdx++) {
  522.                     String deletedName = dels.get(delIdx).oldPath;

  523.                     for (int addIdx = 0; addIdx < adds.size(); addIdx++) {
  524.                         String addedName = adds.get(addIdx).newPath;

  525.                         int score = SimilarityRenameDetector.nameScore(addedName, deletedName);
  526.                         matrix[mNext] = SimilarityRenameDetector.encode(score, delIdx, addIdx);
  527.                         mNext++;
  528.                         if (pm.isCancelled()) {
  529.                             throw new CancelledException(
  530.                                     JGitText.get().renameCancelled);
  531.                         }
  532.                     }
  533.                 }

  534.                 Arrays.sort(matrix);

  535.                 for (--mNext; mNext >= 0; mNext--) {
  536.                     long ent = matrix[mNext];
  537.                     int delIdx = SimilarityRenameDetector.srcFile(ent);
  538.                     int addIdx = SimilarityRenameDetector.dstFile(ent);
  539.                     DiffEntry d = dels.get(delIdx);
  540.                     DiffEntry a = adds.get(addIdx);

  541.                     if (a == null) {
  542.                         advanceOrCancel(pm);
  543.                         continue; // was already matched earlier
  544.                     }

  545.                     ChangeType type;
  546.                     if (d.changeType == ChangeType.DELETE) {
  547.                         // First use of this source file. Tag it as a rename so we
  548.                         // later know it is already been used as a rename, other
  549.                         // matches (if any) will claim themselves as copies instead.
  550.                         //
  551.                         d.changeType = ChangeType.RENAME;
  552.                         type = ChangeType.RENAME;
  553.                     } else {
  554.                         type = ChangeType.COPY;
  555.                     }

  556.                     entries.add(DiffEntry.pair(type, d, a, 100));
  557.                     adds.set(addIdx, null); // Claim the destination was matched.
  558.                     advanceOrCancel(pm);
  559.                 }
  560.             } else {
  561.                 left.addAll(adds);
  562.             }
  563.             advanceOrCancel(pm);
  564.         }
  565.         added = left;

  566.         deleted = new ArrayList<>(deletedMap.size());
  567.         for (Object o : deletedMap.values()) {
  568.             if (o instanceof DiffEntry) {
  569.                 DiffEntry e = (DiffEntry) o;
  570.                 if (e.changeType == ChangeType.DELETE)
  571.                     deleted.add(e);
  572.             } else {
  573.                 List<DiffEntry> list = (List<DiffEntry>) o;
  574.                 for (DiffEntry e : list) {
  575.                     if (e.changeType == ChangeType.DELETE)
  576.                         deleted.add(e);
  577.                 }
  578.             }
  579.         }
  580.         pm.endTask();
  581.     }

  582.     /**
  583.      * Find the best match by file path for a given DiffEntry from a list of
  584.      * DiffEntrys. The returned DiffEntry will be of the same type as <src>. If
  585.      * no DiffEntry can be found that has the same type, this method will return
  586.      * null.
  587.      *
  588.      * @param src
  589.      *            the DiffEntry to try to find a match for
  590.      * @param list
  591.      *            a list of DiffEntrys to search through
  592.      * @return the DiffEntry from <list> who's file path best matches <src>
  593.      */
  594.     private static DiffEntry bestPathMatch(DiffEntry src, List<DiffEntry> list) {
  595.         DiffEntry best = null;
  596.         int score = -1;

  597.         for (DiffEntry d : list) {
  598.             if (sameType(mode(d), mode(src))) {
  599.                 int tmp = SimilarityRenameDetector
  600.                         .nameScore(path(d), path(src));
  601.                 if (tmp > score) {
  602.                     best = d;
  603.                     score = tmp;
  604.                 }
  605.             }
  606.         }

  607.         return best;
  608.     }

  609.     @SuppressWarnings("unchecked")
  610.     private HashMap<AbbreviatedObjectId, Object> populateMap(
  611.             List<DiffEntry> diffEntries, ProgressMonitor pm)
  612.             throws CancelledException {
  613.         HashMap<AbbreviatedObjectId, Object> map = new HashMap<>();
  614.         for (DiffEntry de : diffEntries) {
  615.             Object old = map.put(id(de), de);
  616.             if (old instanceof DiffEntry) {
  617.                 ArrayList<DiffEntry> list = new ArrayList<>(2);
  618.                 list.add((DiffEntry) old);
  619.                 list.add(de);
  620.                 map.put(id(de), list);
  621.             } else if (old != null) {
  622.                 // Must be a list of DiffEntries
  623.                 ((List<DiffEntry>) old).add(de);
  624.                 map.put(id(de), old);
  625.             }
  626.             advanceOrCancel(pm);
  627.         }
  628.         return map;
  629.     }

  630.     private static String path(DiffEntry de) {
  631.         return de.changeType == ChangeType.DELETE ? de.oldPath : de.newPath;
  632.     }

  633.     private static FileMode mode(DiffEntry de) {
  634.         return de.changeType == ChangeType.DELETE ? de.oldMode : de.newMode;
  635.     }

  636.     private static AbbreviatedObjectId id(DiffEntry de) {
  637.         return de.changeType == ChangeType.DELETE ? de.oldId : de.newId;
  638.     }

  639.     static boolean sameType(FileMode a, FileMode b) {
  640.         // Files have to be of the same type in order to rename them.
  641.         // We would never want to rename a file to a gitlink, or a
  642.         // symlink to a file.
  643.         //
  644.         int aType = a.getBits() & FileMode.TYPE_MASK;
  645.         int bType = b.getBits() & FileMode.TYPE_MASK;
  646.         return aType == bType;
  647.     }

  648.     private static DiffEntry exactRename(DiffEntry src, DiffEntry dst) {
  649.         return DiffEntry.pair(ChangeType.RENAME, src, dst, EXACT_RENAME_SCORE);
  650.     }

  651.     private static DiffEntry exactCopy(DiffEntry src, DiffEntry dst) {
  652.         return DiffEntry.pair(ChangeType.COPY, src, dst, EXACT_RENAME_SCORE);
  653.     }
  654. }