/* * 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 git4idea.vfs; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.ObjectsConvertor; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.VcsVFSListener; import com.intellij.openapi.vcs.update.RefreshVFsSynchronously; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileEvent; import com.intellij.ui.AppUIUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.vcsUtil.VcsFileUtil; import com.intellij.vcsUtil.VcsUtil; import git4idea.GitUtil; import git4idea.GitVcs; import git4idea.commands.Git; import git4idea.commands.GitCommand; import git4idea.commands.GitHandler; import git4idea.commands.GitSimpleHandler; import git4idea.i18n.GitBundle; import git4idea.util.GitFileUtils; import org.jetbrains.annotations.NotNull; import java.io.File; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import static com.intellij.util.containers.ContainerUtil.map2Map; import static com.intellij.util.containers.ContainerUtil.newHashSet; public class GitVFSListener extends VcsVFSListener { /** * More than zero if events are suppressed */ private final AtomicInteger myEventsSuppressLevel = new AtomicInteger(0); private final Git myGit; public GitVFSListener(final Project project, final GitVcs vcs, Git git) { super(project, vcs); myGit = git; } /** * Set events suppressed, the events should be unsuppressed later * * @param value true if events should be suppressed, false otherwise */ public void setEventsSuppressed(boolean value) { if (value) { myEventsSuppressLevel.incrementAndGet(); } else { int v = myEventsSuppressLevel.decrementAndGet(); assert v >= 0; } } @Override protected boolean isEventIgnored(VirtualFileEvent event, boolean putInDirty) { return super.isEventIgnored(event, putInDirty) || myEventsSuppressLevel.get() != 0; } protected String getAddTitle() { return GitBundle.getString("vfs.listener.add.title"); } protected String getSingleFileAddTitle() { return GitBundle.getString("vfs.listener.add.single.title"); } protected String getSingleFileAddPromptTemplate() { return GitBundle.getString("vfs.listener.add.single.prompt"); } @Override protected void executeAdd(final List<VirtualFile> addedFiles, final Map<VirtualFile, VirtualFile> copiedFiles) { // Filter added files before further processing Map<VirtualFile, List<VirtualFile>> sortedFiles; try { sortedFiles = GitUtil.sortFilesByGitRoot(addedFiles, true); } catch (VcsException e) { throw new RuntimeException("The exception is not expected here", e); } final HashSet<VirtualFile> retainedFiles = new HashSet<>(); final ProgressManager progressManager = ProgressManager.getInstance(); progressManager.run(new Task.Backgroundable(myProject, GitBundle.getString("vfs.listener.checking.ignored"), true) { @Override public void run(@NotNull ProgressIndicator pi) { for (Map.Entry<VirtualFile, List<VirtualFile>> e : sortedFiles.entrySet()) { VirtualFile root = e.getKey(); List<VirtualFile> files = e.getValue(); pi.setText(root.getPresentableUrl()); try { retainedFiles.addAll(myGit.untrackedFiles(myProject, root, files)); } catch (VcsException ex) { ApplicationManager.getApplication().invokeLater(() -> gitVcs().showMessages(ex.getMessage())); } } addedFiles.retainAll(retainedFiles); AppUIUtil.invokeLaterIfProjectAlive(myProject, () -> originalExecuteAdd(addedFiles, copiedFiles)); } }); } /** * The version of execute add before overriding * * @param addedFiles the added files * @param copiedFiles the copied files */ private void originalExecuteAdd(List<VirtualFile> addedFiles, final Map<VirtualFile, VirtualFile> copiedFiles) { super.executeAdd(addedFiles, copiedFiles); } protected void performAdding(final Collection<VirtualFile> addedFiles, final Map<VirtualFile, VirtualFile> copyFromMap) { // copied files (copyFromMap) are ignored, because they are included into added files. performAdding(ObjectsConvertor.vf2fp(new ArrayList<>(addedFiles))); } private GitVcs gitVcs() { return ((GitVcs)myVcs); } private void performAdding(Collection<FilePath> filesToAdd) { performBackgroundOperation(filesToAdd, GitBundle.getString("add.adding"), new LongOperationPerRootExecutor() { @Override public void execute(@NotNull VirtualFile root, @NotNull List<FilePath> files) throws VcsException { LOG.debug("Git: adding files: " + files); GitFileUtils.addPaths(myProject, root, files); VcsFileUtil.markFilesDirty(myProject, files); } @Override public Collection<File> getFilesToRefresh() { return Collections.emptyList(); } }); } protected String getDeleteTitle() { return GitBundle.getString("vfs.listener.delete.title"); } protected String getSingleFileDeleteTitle() { return GitBundle.getString("vfs.listener.delete.single.title"); } protected String getSingleFileDeletePromptTemplate() { return GitBundle.getString("vfs.listener.delete.single.prompt"); } protected void performDeletion(final List<FilePath> filesToDelete) { performBackgroundOperation(filesToDelete, GitBundle.getString("remove.removing"), new LongOperationPerRootExecutor() { HashSet<File> filesToRefresh = new HashSet<>(); public void execute(@NotNull VirtualFile root, @NotNull List<FilePath> files) throws VcsException { GitFileUtils.delete(myProject, root, files, "--ignore-unmatch", "--cached"); if (!myProject.isDisposed()) { VcsFileUtil.markFilesDirty(myProject, files); } File rootFile = new File(root.getPath()); for (FilePath p : files) { for (File f = p.getIOFile(); f != null && !FileUtil.filesEqual(f, rootFile); f = f.getParentFile()) { filesToRefresh.add(f); } } } public Collection<File> getFilesToRefresh() { return filesToRefresh; } }); } protected void performMoveRename(final List<MovedFileInfo> movedFiles) { List<FilePath> toAdd = ContainerUtil.newArrayList(); List<FilePath> toRemove = ContainerUtil.newArrayList(); List<MovedFileInfo> toForceMove = ContainerUtil.newArrayList(); for (MovedFileInfo movedInfo : movedFiles) { String oldPath = movedInfo.myOldPath; String newPath = movedInfo.myNewPath; if (!SystemInfo.isFileSystemCaseSensitive && GitUtil.isCaseOnlyChange(oldPath, newPath)) { toForceMove.add(movedInfo); } else { toRemove.add(VcsUtil.getFilePath(oldPath)); toAdd.add(VcsUtil.getFilePath(newPath)); } } performAdding(toAdd); performDeletion(toRemove); performForceMove(toForceMove); } private void performForceMove(@NotNull List<MovedFileInfo> files) { Map<FilePath, MovedFileInfo> filesToMove = map2Map(files, (info) -> Pair.create(VcsUtil.getFilePath(info.myNewPath), info)); Set<File> toRefresh = newHashSet(); performBackgroundOperation(filesToMove.keySet(), "Moving Files...", new LongOperationPerRootExecutor() { @Override public void execute(@NotNull VirtualFile root, @NotNull List<FilePath> files) throws VcsException { for (FilePath file : files) { GitHandler h = new GitSimpleHandler(myProject, root, GitCommand.MV); MovedFileInfo info = filesToMove.get(file); h.addParameters("-f", info.myOldPath, info.myNewPath); h.runInCurrentThread(null); toRefresh.add(new File(info.myOldPath)); toRefresh.add(new File(info.myNewPath)); } } @Override public Collection<File> getFilesToRefresh() { return toRefresh; } }); } protected boolean isDirectoryVersioningSupported() { return false; } @Override protected Collection<FilePath> selectFilePathsToDelete(final List<FilePath> deletedFiles) { // For git asking about vcs delete does not make much sense. The result is practically identical. return deletedFiles; } private void performBackgroundOperation(@NotNull Collection<FilePath> files, @NotNull String operationTitle, @NotNull LongOperationPerRootExecutor executor) { Map<VirtualFile, List<FilePath>> sortedFiles; try { sortedFiles = GitUtil.sortFilePathsByGitRoot(files, true); } catch (VcsException e) { gitVcs().showMessages(e.getMessage()); return; } GitVcs.runInBackground(new Task.Backgroundable(myProject, operationTitle) { public void run(@NotNull ProgressIndicator indicator) { for (Map.Entry<VirtualFile, List<FilePath>> e : sortedFiles.entrySet()) { try { executor.execute(e.getKey(), e.getValue()); } catch (final VcsException ex) { ApplicationManager.getApplication().invokeLater(() -> gitVcs().showMessages(ex.getMessage())); } } RefreshVFsSynchronously.refreshFiles(executor.getFilesToRefresh()); } }); } private interface LongOperationPerRootExecutor { void execute(@NotNull VirtualFile root, @NotNull List<FilePath> files) throws VcsException; Collection<File> getFilesToRefresh(); } }