@NotNull
  public static List<Change> zipChanges(@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());
  }
@NotNull
  public static List<Change> zipChanges(@NotNull List<? extends Change> changes)