blob: a3fa9e4e9e9ae70a17cb3512777fc1c7274266b3 [file] [log] [blame]
/*
* Copyright 2000-2010 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 org.zmlx.hg4idea;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.ChangeListManagerImpl;
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.AppUIUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.VcsBackgroundTask;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.zmlx.hg4idea.command.*;
import org.zmlx.hg4idea.util.HgUtil;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
/**
* Listens to VFS events (such as adding or deleting bunch of files) and performs necessary operations with the VCS.
* @author Kirill Likhodedov
*/
public class HgVFSListener extends VcsVFSListener {
private final VcsDirtyScopeManager dirtyScopeManager;
private static final Logger LOG = Logger.getInstance(HgVFSListener.class);
protected HgVFSListener(final Project project, final HgVcs vcs) {
super(project, vcs);
dirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject);
}
@Override
protected String getAddTitle() {
return HgVcsMessages.message("hg4idea.add.title");
}
@Override
protected String getSingleFileAddTitle() {
return HgVcsMessages.message("hg4idea.add.single.title");
}
@Override
protected String getSingleFileAddPromptTemplate() {
return HgVcsMessages.message("hg4idea.add.body");
}
@Override
protected void executeAdd(final List<VirtualFile> addedFiles, final Map<VirtualFile, VirtualFile> copyFromMap) {
// if a file is copied from another repository, then 'hg add' should be used instead of 'hg copy'.
// Thus here we remove such files from the copyFromMap.
for (Iterator<Map.Entry<VirtualFile, VirtualFile>> it = copyFromMap.entrySet().iterator(); it.hasNext(); ) {
final Map.Entry<VirtualFile, VirtualFile> entry = it.next();
final VirtualFile rootFrom = HgUtil.getHgRootOrNull(myProject, entry.getKey());
final VirtualFile rootTo = HgUtil.getHgRootOrNull(myProject, entry.getValue());
if (rootTo == null || !rootTo.equals(rootFrom)) {
it.remove();
}
}
// exclude files which are added to a directory which is not version controlled
for (Iterator<VirtualFile> it = addedFiles.iterator(); it.hasNext(); ) {
if (HgUtil.getHgRootOrNull(myProject, it.next()) == null) {
it.remove();
}
}
// exclude files which are ignored in .hgignore in background and execute adding after that
final Map<VirtualFile, Collection<VirtualFile>> sortedFiles = HgUtil.sortByHgRoots(myProject, addedFiles);
final HashSet<VirtualFile> untrackedFiles = new HashSet<VirtualFile>();
new Task.Backgroundable(myProject, HgVcsMessages.message("hg4idea.progress.checking.ignored"), false) {
@Override
public void run(@NotNull ProgressIndicator pi) {
for (Map.Entry<VirtualFile, Collection<VirtualFile>> e : sortedFiles.entrySet()) {
VirtualFile repo = e.getKey();
final Collection<VirtualFile> files = e.getValue();
pi.setText(repo.getPresentableUrl());
try {
untrackedFiles
.addAll(new HgStatusCommand.Builder(false).unknown(true).build(myProject)
.getHgUntrackedFiles(repo, new ArrayList<VirtualFile>(files)));
}
catch (final VcsException ex) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
((HgVcs)myVcs).showMessageInConsole(ex.getMessage(), ConsoleViewContentType.ERROR_OUTPUT.getAttributes());
}
});
}
}
addedFiles.retainAll(untrackedFiles);
// select files to add if there is something to select
if (!addedFiles.isEmpty() || !copyFromMap.isEmpty()) {
AppUIUtil.invokeLaterIfProjectAlive(myProject, new Runnable() {
@Override
public void run() {
originalExecuteAdd(addedFiles, copyFromMap);
}
});
}
}
}.queue();
}
/**
* 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);
}
@Override
protected void performAdding(final Collection<VirtualFile> addedFiles, final Map<VirtualFile, VirtualFile> copyFromMap) {
(new Task.ConditionalModal(myProject,
HgVcsMessages.message("hg4idea.add.progress"),
false,
VcsConfiguration.getInstance(myProject).getAddRemoveOption() ) {
@Override public void run(@NotNull ProgressIndicator aProgressIndicator) {
final ArrayList<VirtualFile> adds = new ArrayList<VirtualFile>();
final HashMap<VirtualFile, VirtualFile> copies = new HashMap<VirtualFile, VirtualFile>(); // from -> to
//delete unversioned and ignored files from copy source
LOG.assertTrue(myProject != null, "Project is null");
Collection<VirtualFile> unversionedAndIgnoredFiles = new ArrayList<VirtualFile>();
final Map<VirtualFile, Collection<VirtualFile>> sortedSourceFilesByRepos = HgUtil.sortByHgRoots(myProject, copyFromMap.values());
HgStatusCommand statusCommand = new HgStatusCommand.Builder(false).unknown(true).ignored(true).build(myProject);
for (Map.Entry<VirtualFile, Collection<VirtualFile>> entry : sortedSourceFilesByRepos.entrySet()) {
Set<HgChange> changes =
statusCommand.execute(entry.getKey(), ContainerUtil.map(entry.getValue(), new Function<VirtualFile, FilePath>() {
@Override
public FilePath fun(VirtualFile virtualFile) {
return new FilePathImpl(virtualFile);
}
}));
for (HgChange change : changes) {
unversionedAndIgnoredFiles.add(change.afterFile().toFilePath().getVirtualFile());
}
}
copyFromMap.values().removeAll(unversionedAndIgnoredFiles);
// separate adds from copies
for (VirtualFile file : addedFiles) {
if (file.isDirectory()) {
continue;
}
final VirtualFile copyFrom = copyFromMap.get(file);
if (copyFrom != null) {
copies.put(copyFrom, file);
} else {
adds.add(file);
}
}
// add for all files at once
if (!adds.isEmpty()) {
new HgAddCommand(myProject).execute(adds);
}
// copy needs to be run for each file separately
if (!copies.isEmpty()) {
for(Map.Entry<VirtualFile, VirtualFile> copy : copies.entrySet()) {
new HgCopyCommand(myProject).execute(copy.getKey(), copy.getValue());
}
}
for (VirtualFile file : addedFiles) {
dirtyScopeManager.fileDirty(file);
}
}
}).queue();
}
@Override
protected String getDeleteTitle() {
return HgVcsMessages.message("hg4idea.remove.multiple.title");
}
@Override
protected String getSingleFileDeleteTitle() {
return HgVcsMessages.message("hg4idea.remove.single.title");
}
@Override
protected String getSingleFileDeletePromptTemplate() {
return HgVcsMessages.message("hg4idea.remove.single.body");
}
@Override
protected VcsDeleteType needConfirmDeletion(final VirtualFile file) {
return ChangeListManagerImpl.getInstanceImpl(myProject).getUnversionedFiles().contains(file)
? VcsDeleteType.IGNORE
: VcsDeleteType.CONFIRM;
}
protected void executeDelete() {
final List<FilePath> filesToDelete = new ArrayList<FilePath>(myDeletedWithoutConfirmFiles);
final List<FilePath> filesToConfirmDeletion = new ArrayList<FilePath>(myDeletedFiles);
myDeletedWithoutConfirmFiles.clear();
myDeletedFiles.clear();
// skip files which are not under Mercurial
skipNotUnderHg(filesToDelete);
skipNotUnderHg(filesToConfirmDeletion);
// newly added files (which were added to the repo but never committed) should be removed from the VCS,
// but without user confirmation.
for (Iterator<FilePath> it = filesToConfirmDeletion.iterator(); it.hasNext(); ) {
FilePath filePath = it.next();
Change fileChange = ChangeListManager.getInstance(myProject).getChange(filePath);
if (fileChange != null && fileChange.getFileStatus().equals(FileStatus.ADDED)) {
filesToDelete.add(filePath);
it.remove();
}
}
new Task.ConditionalModal(myProject,
HgVcsMessages.message("hg4idea.remove.progress"),
false,
VcsConfiguration.getInstance(myProject).getAddRemoveOption()) {
@Override public void run( @NotNull ProgressIndicator indicator ) {
// confirm removal from the VCS if needed
if (myRemoveOption.getValue() != VcsShowConfirmationOption.Value.DO_NOTHING_SILENTLY) {
if (myRemoveOption.getValue() == VcsShowConfirmationOption.Value.DO_ACTION_SILENTLY || filesToConfirmDeletion.isEmpty()) {
filesToDelete.addAll(filesToConfirmDeletion);
}
else {
final AtomicReference<Collection<FilePath>> filePaths = new AtomicReference<Collection<FilePath>>();
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override public void run() {
filePaths.set(selectFilePathsToDelete(filesToConfirmDeletion));
}
}, indicator.getModalityState());
if (filePaths.get() != null) {
filesToDelete.addAll(filePaths.get());
}
}
}
if (!filesToDelete.isEmpty()) {
performDeletion(filesToDelete);
}
}
}.queue();
}
/**
* Changes the given collection of files by filtering out unversioned files and
* files which are not under Mercurial repository.
*
* @param filesToFilter files to be filtered.
*/
private void skipNotUnderHg(Collection<FilePath> filesToFilter) {
for (Iterator<FilePath> iter = filesToFilter.iterator(); iter.hasNext(); ) {
final FilePath filePath = iter.next();
if (HgUtil.getHgRootOrNull(myProject, filePath) == null) {
iter.remove();
}
}
}
@Override
protected void performDeletion( final List<FilePath> filesToDelete) {
final ArrayList<HgFile> deletes = new ArrayList<HgFile>();
for (FilePath file : filesToDelete) {
if (file.isDirectory()) {
continue;
}
VirtualFile root = VcsUtil.getVcsRootFor(myProject, file);
if (root != null) {
deletes.add(new HgFile(root, file));
}
}
if (!deletes.isEmpty()) {
new HgRemoveCommand(myProject).execute(deletes);
}
for (HgFile file : deletes) {
dirtyScopeManager.fileDirty(file.toFilePath());
}
}
@Override
protected void performMoveRename(List<MovedFileInfo> movedFiles) {
(new VcsBackgroundTask<MovedFileInfo>(myProject,
HgVcsMessages.message("hg4idea.move.progress"),
VcsConfiguration.getInstance(myProject).getAddRemoveOption(),
movedFiles) {
protected void process(final MovedFileInfo file) throws VcsException {
final FilePath source = VcsUtil.getFilePath(file.myOldPath);
final FilePath target = VcsUtil.getFilePath(file.myNewPath);
VirtualFile sourceRoot = VcsUtil.getVcsRootFor(myProject, source);
VirtualFile targetRoot = VcsUtil.getVcsRootFor(myProject, target);
if (sourceRoot != null && targetRoot != null) {
(new HgMoveCommand(myProject)).execute(new HgFile(sourceRoot, source), new HgFile(targetRoot, target));
}
dirtyScopeManager.fileDirty(source);
dirtyScopeManager.fileDirty(target);
}
}).queue();
}
@Override
protected boolean isDirectoryVersioningSupported() {
return false;
}
}