You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

RenameDetector.java 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  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 static org.eclipse.jgit.storage.pack.PackConfig.DEFAULT_BIG_FILE_THRESHOLD;
  14. import java.io.IOException;
  15. import java.util.ArrayList;
  16. import java.util.Arrays;
  17. import java.util.Collection;
  18. import java.util.Collections;
  19. import java.util.Comparator;
  20. import java.util.HashMap;
  21. import java.util.List;
  22. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  23. import org.eclipse.jgit.diff.SimilarityIndex.TableFullException;
  24. import org.eclipse.jgit.errors.CancelledException;
  25. import org.eclipse.jgit.internal.JGitText;
  26. import org.eclipse.jgit.lib.AbbreviatedObjectId;
  27. import org.eclipse.jgit.lib.FileMode;
  28. import org.eclipse.jgit.lib.NullProgressMonitor;
  29. import org.eclipse.jgit.lib.ObjectReader;
  30. import org.eclipse.jgit.lib.ProgressMonitor;
  31. import org.eclipse.jgit.lib.Repository;
  32. /**
  33. * Detect and resolve object renames.
  34. */
  35. public class RenameDetector {
  36. private static final int EXACT_RENAME_SCORE = 100;
  37. private static final Comparator<DiffEntry> DIFF_COMPARATOR = new Comparator<DiffEntry>() {
  38. @Override
  39. public int compare(DiffEntry a, DiffEntry b) {
  40. int cmp = nameOf(a).compareTo(nameOf(b));
  41. if (cmp == 0)
  42. cmp = sortOf(a.getChangeType()) - sortOf(b.getChangeType());
  43. return cmp;
  44. }
  45. private String nameOf(DiffEntry ent) {
  46. // Sort by the new name, unless the change is a delete. On
  47. // deletes the new name is /dev/null, so we sort instead by
  48. // the old name.
  49. //
  50. if (ent.changeType == ChangeType.DELETE)
  51. return ent.oldPath;
  52. return ent.newPath;
  53. }
  54. private int sortOf(ChangeType changeType) {
  55. // Sort deletes before adds so that a major type change for
  56. // a file path (such as symlink to regular file) will first
  57. // remove the path, then add it back with the new type.
  58. //
  59. switch (changeType) {
  60. case DELETE:
  61. return 1;
  62. case ADD:
  63. return 2;
  64. default:
  65. return 10;
  66. }
  67. }
  68. };
  69. private List<DiffEntry> entries;
  70. private List<DiffEntry> deleted;
  71. private List<DiffEntry> added;
  72. private boolean done;
  73. private final ObjectReader objectReader;
  74. /** Similarity score required to pair an add/delete as a rename. */
  75. private int renameScore = 60;
  76. /**
  77. * Similarity score required to keep modified file pairs together. Any
  78. * modified file pairs with a similarity score below this will be broken
  79. * apart.
  80. */
  81. private int breakScore = -1;
  82. /** Limit in the number of files to consider for renames. */
  83. private int renameLimit;
  84. /**
  85. * File size threshold (in bytes) for detecting renames. Files larger
  86. * than this size will not be processed for renames.
  87. */
  88. private int bigFileThreshold = DEFAULT_BIG_FILE_THRESHOLD;
  89. /** Set if the number of adds or deletes was over the limit. */
  90. private boolean overRenameLimit;
  91. /**
  92. * Create a new rename detector for the given repository
  93. *
  94. * @param repo
  95. * the repository to use for rename detection
  96. */
  97. public RenameDetector(Repository repo) {
  98. this(repo.newObjectReader(), repo.getConfig().get(DiffConfig.KEY));
  99. }
  100. /**
  101. * Create a new rename detector with a specified reader and diff config.
  102. *
  103. * @param reader
  104. * reader to obtain objects from the repository with.
  105. * @param cfg
  106. * diff config specifying rename detection options.
  107. * @since 3.0
  108. */
  109. public RenameDetector(ObjectReader reader, DiffConfig cfg) {
  110. objectReader = reader.newReader();
  111. renameLimit = cfg.getRenameLimit();
  112. reset();
  113. }
  114. /**
  115. * Get rename score
  116. *
  117. * @return minimum score required to pair an add/delete as a rename. The
  118. * score ranges are within the bounds of (0, 100).
  119. */
  120. public int getRenameScore() {
  121. return renameScore;
  122. }
  123. /**
  124. * Set the minimum score required to pair an add/delete as a rename.
  125. * <p>
  126. * When comparing two files together their score must be greater than or
  127. * equal to the rename score for them to be considered a rename match. The
  128. * score is computed based on content similarity, so a score of 60 implies
  129. * that approximately 60% of the bytes in the files are identical.
  130. *
  131. * @param score
  132. * new rename score, must be within [0, 100].
  133. * @throws java.lang.IllegalArgumentException
  134. * the score was not within [0, 100].
  135. */
  136. public void setRenameScore(int score) {
  137. if (score < 0 || score > 100)
  138. throw new IllegalArgumentException(
  139. JGitText.get().similarityScoreMustBeWithinBounds);
  140. renameScore = score;
  141. }
  142. /**
  143. * Get break score
  144. *
  145. * @return the similarity score required to keep modified file pairs
  146. * together. Any modify pairs that score below this will be broken
  147. * apart into separate add/deletes. Values less than or equal to
  148. * zero indicate that no modifies will be broken apart. Values over
  149. * 100 cause all modify pairs to be broken.
  150. */
  151. public int getBreakScore() {
  152. return breakScore;
  153. }
  154. /**
  155. * Set break score
  156. *
  157. * @param breakScore
  158. * the similarity score required to keep modified file pairs
  159. * together. Any modify pairs that score below this will be
  160. * broken apart into separate add/deletes. Values less than or
  161. * equal to zero indicate that no modifies will be broken apart.
  162. * Values over 100 cause all modify pairs to be broken.
  163. */
  164. public void setBreakScore(int breakScore) {
  165. this.breakScore = breakScore;
  166. }
  167. /**
  168. * Get rename limit
  169. *
  170. * @return limit on number of paths to perform inexact rename detection
  171. */
  172. public int getRenameLimit() {
  173. return renameLimit;
  174. }
  175. /**
  176. * Set the limit on the number of files to perform inexact rename detection.
  177. * <p>
  178. * The rename detector has to build a square matrix of the rename limit on
  179. * each side, then perform that many file compares to determine similarity.
  180. * If 1000 files are added, and 1000 files are deleted, a 1000*1000 matrix
  181. * must be allocated, and 1,000,000 file compares may need to be performed.
  182. *
  183. * @param limit
  184. * new file limit. 0 means no limit; a negative number means no
  185. * inexact rename detection will be performed, only exact rename
  186. * detection.
  187. */
  188. public void setRenameLimit(int limit) {
  189. renameLimit = limit;
  190. }
  191. /**
  192. * Get file size threshold for detecting renames. Files larger
  193. * than this size will not be processed for rename detection.
  194. *
  195. * @return threshold in bytes of the file size.
  196. * @since 5.12
  197. */
  198. public int getBigFileThreshold() { return bigFileThreshold; }
  199. /**
  200. * Set the file size threshold for detecting renames. Files larger than this
  201. * threshold will be skipped during rename detection computation.
  202. *
  203. * @param threshold file size threshold in bytes.
  204. * @since 5.12
  205. */
  206. public void setBigFileThreshold(int threshold) {
  207. this.bigFileThreshold = threshold;
  208. }
  209. /**
  210. * Check if the detector is over the rename limit.
  211. * <p>
  212. * This method can be invoked either before or after {@code getEntries} has
  213. * been used to perform rename detection.
  214. *
  215. * @return true if the detector has more file additions or removals than the
  216. * rename limit is currently set to. In such configurations the
  217. * detector will skip expensive computation.
  218. */
  219. public boolean isOverRenameLimit() {
  220. if (done)
  221. return overRenameLimit;
  222. int cnt = Math.max(added.size(), deleted.size());
  223. return getRenameLimit() != 0 && getRenameLimit() < cnt;
  224. }
  225. /**
  226. * Add entries to be considered for rename detection.
  227. *
  228. * @param entriesToAdd
  229. * one or more entries to add.
  230. * @throws java.lang.IllegalStateException
  231. * if {@code getEntries} was already invoked.
  232. */
  233. public void addAll(Collection<DiffEntry> entriesToAdd) {
  234. if (done)
  235. throw new IllegalStateException(JGitText.get().renamesAlreadyFound);
  236. for (DiffEntry entry : entriesToAdd) {
  237. switch (entry.getChangeType()) {
  238. case ADD:
  239. added.add(entry);
  240. break;
  241. case DELETE:
  242. deleted.add(entry);
  243. break;
  244. case MODIFY:
  245. if (sameType(entry.getOldMode(), entry.getNewMode())) {
  246. entries.add(entry);
  247. } else {
  248. List<DiffEntry> tmp = DiffEntry.breakModify(entry);
  249. deleted.add(tmp.get(0));
  250. added.add(tmp.get(1));
  251. }
  252. break;
  253. case COPY:
  254. case RENAME:
  255. default:
  256. entries.add(entry);
  257. }
  258. }
  259. }
  260. /**
  261. * Add an entry to be considered for rename detection.
  262. *
  263. * @param entry
  264. * to add.
  265. * @throws java.lang.IllegalStateException
  266. * if {@code getEntries} was already invoked.
  267. */
  268. public void add(DiffEntry entry) {
  269. addAll(Collections.singletonList(entry));
  270. }
  271. /**
  272. * Detect renames in the current file set.
  273. * <p>
  274. * This convenience function runs without a progress monitor.
  275. *
  276. * @return an unmodifiable list of {@link org.eclipse.jgit.diff.DiffEntry}s
  277. * representing all files that have been changed.
  278. * @throws java.io.IOException
  279. * file contents cannot be read from the repository.
  280. */
  281. public List<DiffEntry> compute() throws IOException {
  282. return compute(NullProgressMonitor.INSTANCE);
  283. }
  284. /**
  285. * Detect renames in the current file set.
  286. *
  287. * @param pm
  288. * report progress during the detection phases.
  289. * @return an unmodifiable list of {@link org.eclipse.jgit.diff.DiffEntry}s
  290. * representing all files that have been changed.
  291. * @throws java.io.IOException
  292. * file contents cannot be read from the repository.
  293. * @throws CancelledException
  294. * if rename detection was cancelled
  295. */
  296. // TODO(ms): use org.eclipse.jgit.api.errors.CanceledException in next major
  297. // version
  298. public List<DiffEntry> compute(ProgressMonitor pm)
  299. throws IOException, CancelledException {
  300. if (!done) {
  301. try {
  302. return compute(objectReader, pm);
  303. } finally {
  304. objectReader.close();
  305. }
  306. }
  307. return Collections.unmodifiableList(entries);
  308. }
  309. /**
  310. * Detect renames in the current file set.
  311. *
  312. * @param reader
  313. * reader to obtain objects from the repository with.
  314. * @param pm
  315. * report progress during the detection phases.
  316. * @return an unmodifiable list of {@link org.eclipse.jgit.diff.DiffEntry}s
  317. * representing all files that have been changed.
  318. * @throws java.io.IOException
  319. * file contents cannot be read from the repository.
  320. * @throws CancelledException
  321. * if rename detection was cancelled
  322. */
  323. // TODO(ms): use org.eclipse.jgit.api.errors.CanceledException in next major
  324. // version
  325. public List<DiffEntry> compute(ObjectReader reader, ProgressMonitor pm)
  326. throws IOException, CancelledException {
  327. final ContentSource cs = ContentSource.create(reader);
  328. return compute(new ContentSource.Pair(cs, cs), pm);
  329. }
  330. /**
  331. * Detect renames in the current file set.
  332. *
  333. * @param reader
  334. * reader to obtain objects from the repository with.
  335. * @param pm
  336. * report progress during the detection phases.
  337. * @return an unmodifiable list of {@link org.eclipse.jgit.diff.DiffEntry}s
  338. * representing all files that have been changed.
  339. * @throws java.io.IOException
  340. * file contents cannot be read from the repository.
  341. * @throws CancelledException
  342. * if rename detection was cancelled
  343. */
  344. // TODO(ms): use org.eclipse.jgit.api.errors.CanceledException in next major
  345. // version
  346. public List<DiffEntry> compute(ContentSource.Pair reader, ProgressMonitor pm)
  347. throws IOException, CancelledException {
  348. if (!done) {
  349. done = true;
  350. if (pm == null)
  351. pm = NullProgressMonitor.INSTANCE;
  352. if (0 < breakScore)
  353. breakModifies(reader, pm);
  354. if (!added.isEmpty() && !deleted.isEmpty())
  355. findExactRenames(pm);
  356. if (!added.isEmpty() && !deleted.isEmpty())
  357. findContentRenames(reader, pm);
  358. if (0 < breakScore && !added.isEmpty() && !deleted.isEmpty())
  359. rejoinModifies(pm);
  360. entries.addAll(added);
  361. added = null;
  362. entries.addAll(deleted);
  363. deleted = null;
  364. Collections.sort(entries, DIFF_COMPARATOR);
  365. }
  366. return Collections.unmodifiableList(entries);
  367. }
  368. /**
  369. * Reset this rename detector for another rename detection pass.
  370. */
  371. public void reset() {
  372. entries = new ArrayList<>();
  373. deleted = new ArrayList<>();
  374. added = new ArrayList<>();
  375. done = false;
  376. }
  377. private void advanceOrCancel(ProgressMonitor pm) throws CancelledException {
  378. if (pm.isCancelled()) {
  379. throw new CancelledException(JGitText.get().renameCancelled);
  380. }
  381. pm.update(1);
  382. }
  383. private void breakModifies(ContentSource.Pair reader, ProgressMonitor pm)
  384. throws IOException, CancelledException {
  385. ArrayList<DiffEntry> newEntries = new ArrayList<>(entries.size());
  386. pm.beginTask(JGitText.get().renamesBreakingModifies, entries.size());
  387. for (int i = 0; i < entries.size(); i++) {
  388. DiffEntry e = entries.get(i);
  389. if (e.getChangeType() == ChangeType.MODIFY) {
  390. int score = calculateModifyScore(reader, e);
  391. if (score < breakScore) {
  392. List<DiffEntry> tmp = DiffEntry.breakModify(e);
  393. DiffEntry del = tmp.get(0);
  394. del.score = score;
  395. deleted.add(del);
  396. added.add(tmp.get(1));
  397. } else {
  398. newEntries.add(e);
  399. }
  400. } else {
  401. newEntries.add(e);
  402. }
  403. advanceOrCancel(pm);
  404. }
  405. entries = newEntries;
  406. }
  407. private void rejoinModifies(ProgressMonitor pm) throws CancelledException {
  408. HashMap<String, DiffEntry> nameMap = new HashMap<>();
  409. ArrayList<DiffEntry> newAdded = new ArrayList<>(added.size());
  410. pm.beginTask(JGitText.get().renamesRejoiningModifies, added.size()
  411. + deleted.size());
  412. for (DiffEntry src : deleted) {
  413. nameMap.put(src.oldPath, src);
  414. advanceOrCancel(pm);
  415. }
  416. for (DiffEntry dst : added) {
  417. DiffEntry src = nameMap.remove(dst.newPath);
  418. if (src != null) {
  419. if (sameType(src.oldMode, dst.newMode)) {
  420. entries.add(DiffEntry.pair(ChangeType.MODIFY, src, dst,
  421. src.score));
  422. } else {
  423. nameMap.put(src.oldPath, src);
  424. newAdded.add(dst);
  425. }
  426. } else {
  427. newAdded.add(dst);
  428. }
  429. advanceOrCancel(pm);
  430. }
  431. added = newAdded;
  432. deleted = new ArrayList<>(nameMap.values());
  433. }
  434. private int calculateModifyScore(ContentSource.Pair reader, DiffEntry d)
  435. throws IOException {
  436. try {
  437. SimilarityIndex src = new SimilarityIndex();
  438. src.hash(reader.open(OLD, d));
  439. src.sort();
  440. SimilarityIndex dst = new SimilarityIndex();
  441. dst.hash(reader.open(NEW, d));
  442. dst.sort();
  443. return src.score(dst, 100);
  444. } catch (TableFullException tableFull) {
  445. // If either table overflowed while being constructed, don't allow
  446. // the pair to be broken. Returning 1 higher than breakScore will
  447. // ensure its not similar, but not quite dissimilar enough to break.
  448. //
  449. overRenameLimit = true;
  450. return breakScore + 1;
  451. }
  452. }
  453. private void findContentRenames(ContentSource.Pair reader,
  454. ProgressMonitor pm)
  455. throws IOException, CancelledException {
  456. int cnt = Math.max(added.size(), deleted.size());
  457. if (getRenameLimit() == 0 || cnt <= getRenameLimit()) {
  458. SimilarityRenameDetector d;
  459. d = new SimilarityRenameDetector(reader, deleted, added);
  460. d.setRenameScore(getRenameScore());
  461. d.setBigFileThreshold(getBigFileThreshold());
  462. d.compute(pm);
  463. overRenameLimit |= d.isTableOverflow();
  464. deleted = d.getLeftOverSources();
  465. added = d.getLeftOverDestinations();
  466. entries.addAll(d.getMatches());
  467. } else {
  468. overRenameLimit = true;
  469. }
  470. }
  471. @SuppressWarnings("unchecked")
  472. private void findExactRenames(ProgressMonitor pm)
  473. throws CancelledException {
  474. pm.beginTask(JGitText.get().renamesFindingExact, //
  475. added.size() + added.size() + deleted.size()
  476. + added.size() * deleted.size());
  477. HashMap<AbbreviatedObjectId, Object> deletedMap = populateMap(deleted, pm);
  478. HashMap<AbbreviatedObjectId, Object> addedMap = populateMap(added, pm);
  479. ArrayList<DiffEntry> uniqueAdds = new ArrayList<>(added.size());
  480. ArrayList<List<DiffEntry>> nonUniqueAdds = new ArrayList<>();
  481. for (Object o : addedMap.values()) {
  482. if (o instanceof DiffEntry)
  483. uniqueAdds.add((DiffEntry) o);
  484. else
  485. nonUniqueAdds.add((List<DiffEntry>) o);
  486. }
  487. ArrayList<DiffEntry> left = new ArrayList<>(added.size());
  488. for (DiffEntry a : uniqueAdds) {
  489. Object del = deletedMap.get(a.newId);
  490. if (del instanceof DiffEntry) {
  491. // We have one add to one delete: pair them if they are the same
  492. // type
  493. DiffEntry e = (DiffEntry) del;
  494. if (sameType(e.oldMode, a.newMode)) {
  495. e.changeType = ChangeType.RENAME;
  496. entries.add(exactRename(e, a));
  497. } else {
  498. left.add(a);
  499. }
  500. } else if (del != null) {
  501. // We have one add to many deletes: find the delete with the
  502. // same type and closest name to the add, then pair them
  503. List<DiffEntry> list = (List<DiffEntry>) del;
  504. DiffEntry best = bestPathMatch(a, list);
  505. if (best != null) {
  506. best.changeType = ChangeType.RENAME;
  507. entries.add(exactRename(best, a));
  508. } else {
  509. left.add(a);
  510. }
  511. } else {
  512. left.add(a);
  513. }
  514. advanceOrCancel(pm);
  515. }
  516. for (List<DiffEntry> adds : nonUniqueAdds) {
  517. Object o = deletedMap.get(adds.get(0).newId);
  518. if (o instanceof DiffEntry) {
  519. // We have many adds to one delete: find the add with the same
  520. // type and closest name to the delete, then pair them. Mark the
  521. // rest as copies of the delete.
  522. DiffEntry d = (DiffEntry) o;
  523. DiffEntry best = bestPathMatch(d, adds);
  524. if (best != null) {
  525. d.changeType = ChangeType.RENAME;
  526. entries.add(exactRename(d, best));
  527. for (DiffEntry a : adds) {
  528. if (a != best) {
  529. if (sameType(d.oldMode, a.newMode)) {
  530. entries.add(exactCopy(d, a));
  531. } else {
  532. left.add(a);
  533. }
  534. }
  535. }
  536. } else {
  537. left.addAll(adds);
  538. }
  539. } else if (o != null) {
  540. // We have many adds to many deletes: score all the adds against
  541. // all the deletes by path name, take the best matches, pair
  542. // them as renames, then call the rest copies
  543. List<DiffEntry> dels = (List<DiffEntry>) o;
  544. long[] matrix = new long[dels.size() * adds.size()];
  545. int mNext = 0;
  546. for (int delIdx = 0; delIdx < dels.size(); delIdx++) {
  547. String deletedName = dels.get(delIdx).oldPath;
  548. for (int addIdx = 0; addIdx < adds.size(); addIdx++) {
  549. String addedName = adds.get(addIdx).newPath;
  550. int score = SimilarityRenameDetector.nameScore(addedName, deletedName);
  551. matrix[mNext] = SimilarityRenameDetector.encode(score, delIdx, addIdx);
  552. mNext++;
  553. if (pm.isCancelled()) {
  554. throw new CancelledException(
  555. JGitText.get().renameCancelled);
  556. }
  557. }
  558. }
  559. Arrays.sort(matrix);
  560. for (--mNext; mNext >= 0; mNext--) {
  561. long ent = matrix[mNext];
  562. int delIdx = SimilarityRenameDetector.srcFile(ent);
  563. int addIdx = SimilarityRenameDetector.dstFile(ent);
  564. DiffEntry d = dels.get(delIdx);
  565. DiffEntry a = adds.get(addIdx);
  566. if (a == null) {
  567. advanceOrCancel(pm);
  568. continue; // was already matched earlier
  569. }
  570. ChangeType type;
  571. if (d.changeType == ChangeType.DELETE) {
  572. // First use of this source file. Tag it as a rename so we
  573. // later know it is already been used as a rename, other
  574. // matches (if any) will claim themselves as copies instead.
  575. //
  576. d.changeType = ChangeType.RENAME;
  577. type = ChangeType.RENAME;
  578. } else {
  579. type = ChangeType.COPY;
  580. }
  581. entries.add(DiffEntry.pair(type, d, a, 100));
  582. adds.set(addIdx, null); // Claim the destination was matched.
  583. advanceOrCancel(pm);
  584. }
  585. } else {
  586. left.addAll(adds);
  587. }
  588. advanceOrCancel(pm);
  589. }
  590. added = left;
  591. deleted = new ArrayList<>(deletedMap.size());
  592. for (Object o : deletedMap.values()) {
  593. if (o instanceof DiffEntry) {
  594. DiffEntry e = (DiffEntry) o;
  595. if (e.changeType == ChangeType.DELETE)
  596. deleted.add(e);
  597. } else {
  598. List<DiffEntry> list = (List<DiffEntry>) o;
  599. for (DiffEntry e : list) {
  600. if (e.changeType == ChangeType.DELETE)
  601. deleted.add(e);
  602. }
  603. }
  604. }
  605. pm.endTask();
  606. }
  607. /**
  608. * Find the best match by file path for a given DiffEntry from a list of
  609. * DiffEntrys. The returned DiffEntry will be of the same type as <src>. If
  610. * no DiffEntry can be found that has the same type, this method will return
  611. * null.
  612. *
  613. * @param src
  614. * the DiffEntry to try to find a match for
  615. * @param list
  616. * a list of DiffEntrys to search through
  617. * @return the DiffEntry from <list> who's file path best matches <src>
  618. */
  619. private static DiffEntry bestPathMatch(DiffEntry src, List<DiffEntry> list) {
  620. DiffEntry best = null;
  621. int score = -1;
  622. for (DiffEntry d : list) {
  623. if (sameType(mode(d), mode(src))) {
  624. int tmp = SimilarityRenameDetector
  625. .nameScore(path(d), path(src));
  626. if (tmp > score) {
  627. best = d;
  628. score = tmp;
  629. }
  630. }
  631. }
  632. return best;
  633. }
  634. @SuppressWarnings("unchecked")
  635. private HashMap<AbbreviatedObjectId, Object> populateMap(
  636. List<DiffEntry> diffEntries, ProgressMonitor pm)
  637. throws CancelledException {
  638. HashMap<AbbreviatedObjectId, Object> map = new HashMap<>();
  639. for (DiffEntry de : diffEntries) {
  640. Object old = map.put(id(de), de);
  641. if (old instanceof DiffEntry) {
  642. ArrayList<DiffEntry> list = new ArrayList<>(2);
  643. list.add((DiffEntry) old);
  644. list.add(de);
  645. map.put(id(de), list);
  646. } else if (old != null) {
  647. // Must be a list of DiffEntries
  648. ((List<DiffEntry>) old).add(de);
  649. map.put(id(de), old);
  650. }
  651. advanceOrCancel(pm);
  652. }
  653. return map;
  654. }
  655. private static String path(DiffEntry de) {
  656. return de.changeType == ChangeType.DELETE ? de.oldPath : de.newPath;
  657. }
  658. private static FileMode mode(DiffEntry de) {
  659. return de.changeType == ChangeType.DELETE ? de.oldMode : de.newMode;
  660. }
  661. private static AbbreviatedObjectId id(DiffEntry de) {
  662. return de.changeType == ChangeType.DELETE ? de.oldId : de.newId;
  663. }
  664. static boolean sameType(FileMode a, FileMode b) {
  665. // Files have to be of the same type in order to rename them.
  666. // We would never want to rename a file to a gitlink, or a
  667. // symlink to a file.
  668. //
  669. int aType = a.getBits() & FileMode.TYPE_MASK;
  670. int bType = b.getBits() & FileMode.TYPE_MASK;
  671. return aType == bType;
  672. }
  673. private static DiffEntry exactRename(DiffEntry src, DiffEntry dst) {
  674. return DiffEntry.pair(ChangeType.RENAME, src, dst, EXACT_RENAME_SCORE);
  675. }
  676. private static DiffEntry exactCopy(DiffEntry src, DiffEntry dst) {
  677. return DiffEntry.pair(ChangeType.COPY, src, dst, EXACT_RENAME_SCORE);
  678. }
  679. }