List<Change> (@NotNull List<? extends Change> changes) { // TODO: further improvements needed // We may want to process collisions more consistent // Possible solution: avoid creating duplicate entries for the same FilePath. No changes in the output should have same beforePath or afterPath. // We may take earliest and latest revisions for each file. // // The main problem would be to keep existing movements in non-conflicting cases (where input changes are taken from linear sequence of commits) // case1: "a -> b; b -> c" - file renamed twice in the same revision (as source and as target) // case2: "a -> b" "b -> c" - file renamed twice in consequent commits // case3: "a -> b; b -> a" - files swapped vs "a -> b" "b -> a" - file rename canceled // case4: "delete a" "b -> a" "modify a" // ... // but return "good enough" result for input with conflicting changes // case1: "new a", "new a" // case2: "a -> b", "new b" // ... // // getting "actually good" results is impossible without knowledge of commits topology. // key - after path (nullable) MultiMap<FilePath, Change> map = MultiMap.createLinked(); for (Change change : changes) { ContentRevision bRev = change.getBeforeRevision(); ContentRevision aRev = change.getAfterRevision(); FilePath bPath = bRev != null ? bRev.getFile() : null; FilePath aPath = aRev != null ? aRev.getFile() : null; if (bRev == null) { map.putValue(aPath, change); continue; } Collection<Change> bucket = map.get(bPath); if (bucket.isEmpty()) { map.putValue(aPath, change); continue; } Change oldChange = bucket.iterator().next(); bucket.remove(oldChange); ContentRevision oldRevision = oldChange.getBeforeRevision(); if (oldRevision != null || aRev != null) { map.putValue(aPath, new Change(oldRevision, aRev)); } } // put deletions into appropriate place in list Collection<Change> deleted = map.remove(null); if (deleted != null) { for (Change change : deleted) { //noinspection ConstantConditions map.putValue(change.getBeforeRevision().getFile(), change); } } return new ArrayList<>(map.values()); }