public static boolean isEquals(@NotNull Change change1, @NotNull Change change2) {
    if (!Comparing.equal(ChangesUtil.getBeforePath(change1), ChangesUtil.getBeforePath(change2)) ||
        !Comparing.equal(ChangesUtil.getAfterPath(change1), ChangesUtil.getAfterPath(change2))) {
      // we use file paths for hashCode, so removing this check might violate comparison contract
      return false;
    }

    for (ChangeDiffViewerWrapperProvider provider : ChangeDiffViewerWrapperProvider.EP_NAME.getExtensions()) {
      ThreeState equals = provider.isEquals(change1, change2);
      if (equals == ThreeState.NO) return false;
    }
    for (ChangeDiffRequestProvider provider : ChangeDiffRequestProvider.EP_NAME.getExtensions()) {
      ThreeState equals = provider.isEquals(change1, change2);
      if (equals == ThreeState.YES) return true;
      if (equals == ThreeState.NO) return false;
    }

    if (!Comparing.equal(change1.getClass(), change2.getClass())) return false;
    if (!Comparing.equal(change1.getFileStatus(), change2.getFileStatus())) return false;
    if (!isEquals(change1.getBeforeRevision(), change2.getBeforeRevision())) return false;
    if (!isEquals(change1.getAfterRevision(), change2.getAfterRevision())) return false;

    if (change1 instanceof ChangeListChange || change2 instanceof ChangeListChange) {
      assert change1 instanceof ChangeListChange && change2 instanceof ChangeListChange;
      String changelistId1 = ((ChangeListChange)change1).getChangeListId();
      String changelistId2 = ((ChangeListChange)change2).getChangeListId();
      if (!Objects.equals(changelistId1, changelistId2)) return false;
    }

    return true;
  }
public static boolean isEquals(@NotNull Change change1, @NotNull Change change2)