/*
* Copyright 2000-2009 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.vcs.changes;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.AbstractVcs;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.Navigatable;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcsUtil.VcsUtil;
import gnu.trove.THashSet;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
import java.util.stream.Stream;
import static com.intellij.util.containers.ContainerUtil.newArrayList;
import static com.intellij.util.containers.ContainerUtil.newTroveSet;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;
/**
* @author max
*/
public class ChangesUtil {
private static final Key<Boolean> INTERNAL_OPERATION_KEY = Key.create("internal vcs operation");
public static final TObjectHashingStrategy<FilePath> FILE_PATH_BY_PATH_ONLY_HASHING_STRATEGY = new TObjectHashingStrategy<FilePath>() {
@Override
public int computeHashCode(@NotNull FilePath path) {
return path.getPath().hashCode();
}
@Override
public boolean equals(@NotNull FilePath path1, @NotNull FilePath path2) {
return StringUtil.equals(path1.getPath(), path2.getPath());
}
};
private ChangesUtil() {}
@NotNull
public static FilePath getFilePath(@NotNull Change change) {
ContentRevision revision = change.getAfterRevision();
if (revision == null) {
revision = change.getBeforeRevision();
assert revision != null;
}
return revision.getFile();
}
@Nullable
public static FilePath getBeforePath(@NotNull Change change) {
ContentRevision revision = change.getBeforeRevision();
return revision == null ? null : revision.getFile();
}
@Nullable
public static FilePath getAfterPath(@NotNull Change change) {
ContentRevision revision = change.getAfterRevision();
return revision == null ? null : revision.getFile();
}
@Nullable
public static AbstractVcs getVcsForChange(@NotNull Change change, @NotNull Project project) {
AbstractVcs result = ChangeListManager.getInstance(project).getVcsFor(change);
return result != null ? result : ProjectLevelVcsManager.getInstance(project).getVcsFor(getFilePath(change));
}
@NotNull
public static Set<AbstractVcs> getAffectedVcses(@NotNull Collection<Change> changes, @NotNull Project project) {
return ContainerUtil.map2SetNotNull(changes, change -> getVcsForChange(change, project));
}
@Nullable
public static AbstractVcs getVcsForFile(@NotNull VirtualFile file, @NotNull Project project) {
return ProjectLevelVcsManager.getInstance(project).getVcsFor(file);
}
@Nullable
public static AbstractVcs getVcsForFile(@NotNull File file, @NotNull Project project) {
return ProjectLevelVcsManager.getInstance(project).getVcsFor(VcsUtil.getFilePath(file));
}
@NotNull
public static List<FilePath> getPaths(@NotNull Collection<Change> changes) {
THashSet<FilePath> distinctPaths = getAllPaths(changes.stream())
.collect(toCollection(() -> newTroveSet(FILE_PATH_BY_PATH_ONLY_HASHING_STRATEGY)));
return newArrayList(distinctPaths);
}
@NotNull
public static List<File> getIoFilesFromChanges(@NotNull Collection<Change> changes) {
return getAllPaths(changes.stream())
.map(FilePath::getIOFile)
.distinct()
.collect(toList());
}
@NotNull
public static Stream<FilePath> getAllPaths(@NotNull Stream<Change> changes) {
return changes
.flatMap(change -> Stream.of(getBeforePath(change), getAfterPath(change)))
.filter(Objects::nonNull);
}
/**
* @deprecated Use {@link ChangesUtil#getAfterRevisionsFiles(Stream)}.
*/
@SuppressWarnings("unused") // Required for compatibility with external plugins.
@Deprecated
@NotNull
public static VirtualFile[] getFilesFromChanges(@NotNull Collection<Change> changes) {
return getAfterRevisionsFiles(changes.stream()).toArray(VirtualFile[]::new);
}
@NotNull
public static Stream<VirtualFile> getAfterRevisionsFiles(@NotNull Stream<Change> changes) {
return getAfterRevisionsFiles(changes, false);
}
@NotNull
public static Stream<VirtualFile> getAfterRevisionsFiles(@NotNull Stream<Change> changes, boolean refresh) {
LocalFileSystem fileSystem = LocalFileSystem.getInstance();
return changes
.map(Change::getAfterRevision)
.filter(Objects::nonNull)
.map(ContentRevision::getFile)
.map(path -> refresh ? fileSystem.refreshAndFindFileByPath(path.getPath()) : path.getVirtualFile())
.filter(Objects::nonNull)
.filter(VirtualFile::isValid);
}
@NotNull
public static Navigatable[] getNavigatableArray(@NotNull Project project, @NotNull VirtualFile[] files) {
return getNavigatableArray(project, Stream.of(files));
}
@NotNull
public static Navigatable[] getNavigatableArray(@NotNull Project project, @NotNull Stream<VirtualFile> files) {
return files
.filter(file -> !file.isDirectory())
.map(file -> new OpenFileDescriptor(project, file))
.toArray(Navigatable[]::new);
}
@Nullable
public static ChangeList getChangeListIfOnlyOne(@NotNull Project project, @Nullable Change[] changes) {
ChangeListManager manager = ChangeListManager.getInstance(project);
String changeListName = manager.getChangeListNameIfOnlyOne(changes);
return changeListName == null ? null : manager.findChangeList(changeListName);
}
public static FilePath getCommittedPath(@NotNull Project project, FilePath filePath) {
// check if the file has just been renamed (IDEADEV-15494)
Change change = ChangeListManager.getInstance(project).getChange(filePath);
if (change != null) {
ContentRevision beforeRevision = change.getBeforeRevision();
ContentRevision afterRevision = change.getAfterRevision();
if (beforeRevision != null && afterRevision != null && !beforeRevision.getFile().equals(afterRevision.getFile()) &&
afterRevision.getFile().equals(filePath)) {
filePath = beforeRevision.getFile();
}
}
return filePath;
}
public static FilePath getLocalPath(@NotNull Project project, FilePath filePath) {
// check if the file has just been renamed (IDEADEV-15494)
Change change = ReadAction.compute(() -> {
if (project.isDisposed()) throw new ProcessCanceledException();
return ChangeListManager.getInstance(project).getChange(filePath);
});
if (change != null) {
ContentRevision beforeRevision = change.getBeforeRevision();
ContentRevision afterRevision = change.getAfterRevision();
if (beforeRevision != null && afterRevision != null && !beforeRevision.getFile().equals(afterRevision.getFile()) &&
beforeRevision.getFile().equals(filePath)) {
return afterRevision.getFile();
}
}
return filePath;
}
@Nullable
public static VirtualFile findValidParentUnderReadAction(@NotNull FilePath path) {
VirtualFile file = path.getVirtualFile();
return file != null ? file : getValidParentUnderReadAction(path);
}
@Nullable
public static VirtualFile findValidParentAccurately(@NotNull FilePath filePath) {
VirtualFile result = filePath.getVirtualFile();
if (result == null && !ApplicationManager.getApplication().isReadAccessAllowed()) {
result = LocalFileSystem.getInstance().refreshAndFindFileByPath(filePath.getPath());
}
if (result == null) {
result = getValidParentUnderReadAction(filePath);
}
return result;
}
@Nullable
private static VirtualFile getValidParentUnderReadAction(@NotNull FilePath filePath) {
return ReadAction.compute(() -> {
VirtualFile result = null;
FilePath parent = filePath;
LocalFileSystem lfs = LocalFileSystem.getInstance();
while (result == null && parent != null) {
result = lfs.findFileByPath(parent.getPath());
parent = parent.getParentPath();
}
return result;
});
}
@Nullable
public static String getProjectRelativePath(@NotNull Project project, @Nullable File fileName) {
if (fileName == null) return null;
VirtualFile baseDir = project.getBaseDir();
if (baseDir == null) return fileName.toString();
String relativePath = FileUtil.getRelativePath(VfsUtilCore.virtualToIoFile(baseDir), fileName);
if (relativePath != null) return relativePath;
return fileName.toString();
}
public static boolean isBinaryContentRevision(@Nullable ContentRevision revision) {
return revision instanceof BinaryContentRevision && !revision.getFile().isDirectory();
}
public static boolean isBinaryChange(@NotNull Change change) {
return isBinaryContentRevision(change.getBeforeRevision()) || isBinaryContentRevision(change.getAfterRevision());
}
public static boolean isTextConflictingChange(@NotNull Change change) {
FileStatus status = change.getFileStatus();
return FileStatus.MERGED_WITH_CONFLICTS.equals(status) || FileStatus.MERGED_WITH_BOTH_CONFLICTS.equals(status);
}
@FunctionalInterface
public interface PerVcsProcessor<T> {
void process(@NotNull AbstractVcs vcs, @NotNull List<T> items);
}
@FunctionalInterface
public interface VcsSeparator<T> {
@Nullable
AbstractVcs getVcsFor(@NotNull T item);
}
public static <T> void processItemsByVcs(@NotNull Collection<T> items,
@NotNull VcsSeparator<T> separator,
@NotNull PerVcsProcessor<T> processor) {
final Map<AbstractVcs, List<T>> changesByVcs = new HashMap<>();
for (T item : items) {
final AbstractVcs vcs = separator.getVcsFor(item);
if (vcs != null) {
List<T> vcsChanges = changesByVcs.get(vcs);
if (vcsChanges == null) {
vcsChanges = new ArrayList<>();
changesByVcs.put(vcs, vcsChanges);
}
vcsChanges.add(item);
}
}
for (Map.Entry<AbstractVcs, List<T>> entry : changesByVcs.entrySet()) {
processor.process(entry.getKey(), entry.getValue());
}
}
public static void processChangesByVcs(@NotNull Project project,
@NotNull Collection<Change> changes,
@NotNull PerVcsProcessor<Change> processor) {
processItemsByVcs(changes, change -> getVcsForChange(change, project), processor);
}
public static void processVirtualFilesByVcs(@NotNull Project project,
@NotNull Collection<VirtualFile> files,
@NotNull PerVcsProcessor<VirtualFile> processor) {
processItemsByVcs(files, file -> getVcsForFile(file, project), processor);
}
public static void processFilePathsByVcs(@NotNull Project project,
@NotNull Collection<FilePath> files,
@NotNull PerVcsProcessor<FilePath> processor) {
processItemsByVcs(files, filePath -> getVcsForFile(filePath.getIOFile(), project), processor);
}
@NotNull
public static List<File> filePathsToFiles(@NotNull Collection<FilePath> filePaths) {
return filePaths.stream()
.map(FilePath::getIOFile)
.collect(toList());
}
public static boolean hasFileChanges(@NotNull Collection<Change> changes) {
return changes.stream()
.map(ChangesUtil::getFilePath)
.anyMatch(path -> !path.isDirectory());
}
public static void markInternalOperation(@NotNull Iterable<Change> changes, boolean set) {
for (Change change : changes) {
VirtualFile file = change.getVirtualFile();
if (file != null) {
markInternalOperation(file, set);
}
}
}
public static void markInternalOperation(@NotNull VirtualFile file, boolean set) {
file.putUserData(INTERNAL_OPERATION_KEY, set);
}
public static boolean isInternalOperation(@NotNull VirtualFile file) {
return Boolean.TRUE.equals(file.getUserData(INTERNAL_OPERATION_KEY));
}
/**
* Find common ancestor for changes (included both before and after files)
*/
@Nullable
public static File findCommonAncestor(@NotNull Collection<Change> changes) {
File ancestor = null;
for (Change change : changes) {
File currentChangeAncestor = getCommonBeforeAfterAncestor(change);
if (currentChangeAncestor == null) return null;
if (ancestor == null) {
ancestor = currentChangeAncestor;
}
else {
ancestor = FileUtil.findAncestor(ancestor, currentChangeAncestor);
if (ancestor == null) return null;
}
}
return ancestor;
}
@Nullable
private static File getCommonBeforeAfterAncestor(@NotNull Change change) {
FilePath before = getBeforePath(change);
FilePath after = getAfterPath(change);
return before == null
? ObjectUtils.assertNotNull(after).getIOFile()
: after == null ? before.getIOFile() : FileUtil.findAncestor(before.getIOFile(), after.getIOFile());
}
}