/*
* 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 org.community.intellij.plugins.communitycase.changes;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderEnumerator;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.VcsDirtyScope;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.vcsUtil.VcsUtil;
import org.community.intellij.plugins.communitycase.ContentRevision;
import org.community.intellij.plugins.communitycase.Util;
import org.community.intellij.plugins.communitycase.Vcs;
import org.community.intellij.plugins.communitycase.commands.Command;
import org.community.intellij.plugins.communitycase.commands.Handler;
import org.community.intellij.plugins.communitycase.commands.SimpleHandler;
import org.community.intellij.plugins.communitycase.config.VcsSettings;
import org.community.intellij.plugins.communitycase.history.HistoryUtils;
import org.community.intellij.plugins.communitycase.i18n.Bundle;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.*;
import java.util.regex.Pattern;
//todo wc clean up this shitty shitty code
//todo wc changes requested once for each module, with the module as myVcsRoot. Account for this and make sure we're compatible
/**
* A collector for changes in version control. It is introduced because changes are not
* cannot be got as a sum of stateless operations.
*
===================
ct lsco -a -me -cvi
5 seconds bamain from anywhere in tree
format:
--12-21T17:09 ascher checkout version "C:\cc\baplugintest\serverdev\lost+found\wrapper_32.6223c92a584d4266bcc1b081259b9b2c" from \main\0 (reserved)
"CHECKOUT COMMENT!!!"
===================
update -print
9 minutes bamain
===================
ls -r
35 minutes for bamain
===================
*
*
* dir not checked out, file checked out, file deleted -> deleted (CC won't restore the file on an update)
* dir not checked out, file not checked out, file deleted -> missing (CC will restore the file on an update) (deleted from fs status)
*/
class ChangeCollector {
private static final String VERSION_KEY = "@@";
private static final Logger log=Logger.getInstance("#"+ChangeCollector.class.getName());
private final Project myProject;
private final ChangeListManager myChangeListManager;
//private final ProgressIndicator myProgressIndicator;
private final VcsDirtyScope myDirtyScope;
private final ProjectFileIndex myFileIndex;
private final VirtualFile myRoot; //will be one of the dirty paths
private final List<VirtualFile> myUnversioned = new ArrayList<VirtualFile>(); // Unversioned files
private final List<Change> myChanges = new ArrayList<Change>(); // all changes
private boolean myIsCollected = false; // indicates that collecting changes has been started
private static final int MAX_THREADS=15;
private final List<RecurseRunnable> myRecurseThreads=new ArrayList<RecurseRunnable>();
private final List<LsRunnable> myLsThreads=new ArrayList<LsRunnable>();
private Pattern myPathFilter=null;
public ChangeCollector(final Project project,
ChangeListManager changeListManager,
final ProgressIndicator progressIndicator,
VcsDirtyScope dirtyScope,
final VirtualFile root) {
myChangeListManager = changeListManager;
//myProgressIndicator = progressIndicator;
myDirtyScope = dirtyScope;
myRoot=root;
myProject = project;
myFileIndex=ProjectRootManager.getInstance(myProject).getFileIndex();
}
/**
* Get unversioned files
* @return the changes
* @throws com.intellij.openapi.vcs.VcsException in several cases
*/
public Collection<VirtualFile> unversioned() throws VcsException {
ensureCollected();
return myUnversioned;
}
/**
* Get changes
* @throws com.intellij.openapi.vcs.VcsException in several cases
* @return the changes
*/
public Collection<Change> changes() throws VcsException {
ensureCollected();
return myChanges;
}
/**
* Ensure that changes has been collected.
* @throws com.intellij.openapi.vcs.VcsException in several cases
*/
private void ensureCollected() throws VcsException {
if(!myIsCollected) {
myIsCollected = true;
for(int i=0; i<MAX_THREADS; i++) {
myLsThreads.add(new LsRunnable());
}
if(VcsSettings.getInstance(myProject)!=null && !VcsSettings.getInstance(myProject).getPathFilter().isEmpty()) {
try {
//disable the inspection, we check if null above.
//noinspection ConstantConditions
myPathFilter=Pattern.compile(VcsSettings.getInstance(myProject).getPathFilter());
} catch(Exception e) {
throw new VcsException(Bundle.getString("vcs.config.pathfilter.badregex"),e);
}
}
collectVcsModifiedList();
if(!DumbService.getInstance(myProject).isDumb()) { //don't go to town on the HD if we're indexing, causes excessive thrashing
Map<String,VirtualFile> vfMap=Util.stringToVirtualFile(myRoot,
getFsWritableFiles(),
true);
for(Iterator<Map.Entry<String,VirtualFile>> i=vfMap.entrySet().iterator(); i.hasNext();) {
Map.Entry<String,VirtualFile> pair=i.next();
if(pair.getValue() == null) {
popupNotification(NotificationType.WARNING, Bundle.message("changes.ls.mappingerr.content", pair.getKey()));
i.remove();
}
}
Collection<VirtualFile> addedOrHijackedOrCheckedOutFiles=vfMap.values();
//we already have all the info we need about checked out files, so remove those from the list
for(Change change:myChanges)
addedOrHijackedOrCheckedOutFiles.remove(change.getVirtualFile()); //remove files already in the checkout list. We don't need to check the status; we already know it.
//VirtualFile has no equals method, but since it is unique for this
//IntelliJ aka VM instance, we're ok to use Object's default one with the Set (Sets compare elements for equality)
chunkCheckAdd(addedOrHijackedOrCheckedOutFiles);
synchronized(myLsThreads) {
while(true) { //wait until all threads have exited
if(myLsThreads.size()==MAX_THREADS)
break;
try {
myLsThreads.wait();
} catch(InterruptedException ignored) {}
}
}
myLsThreads.clear();
} else { //if we're in dumb mode, trigger a refresh after all files are indexed
//todo wc don't register this listener several times! Put a variable in ChangeProvider that can remember if there's one installed.
myProject.getMessageBus().connect().subscribe(DumbService.DUMB_MODE, new DumbService.DumbModeListener() {
public void enteredDumbMode() {}
public void exitDumbMode() {
//VcsUtil.runVcsProcessWithProgress()
HashSet<FilePath> paths=new HashSet<FilePath>();
paths.add(Util.virtualFileToFilePath(myRoot));
VcsUtil.refreshFiles(myProject, paths);
}
});
}
}
}
/*
private Collection<VirtualFile> getProjectPaneFiles() {
HashSet<VirtualFile> accumulator=new HashSet<VirtualFile>();
ProjectView pv=ProjectView.getInstance(myProject);
DefaultMutableTreeNode rootNode=pv.getProjectViewPaneById("ProjectPane").getTreeBuilder().getRootNode();
recurseTreeAndAddFiles(accumulator,rootNode);
return accumulator;
}
private void recurseTreeAndAddFiles(HashSet<VirtualFile> accumulator,@NotNull DefaultMutableTreeNode node) {
Object userObject=node.getUserObject();
if(userObject !=null && userObject instanceof PsiFileSystemItem)
accumulator.add(((PsiFileSystemItem)userObject).getVirtualFile());
Enumeration<DefaultMutableTreeNode> i=node.children();
while(i.hasMoreElements()) {
recurseTreeAndAddFiles(accumulator,i.nextElement());
}
}
*/
/*
private void collectProjectFiles() {
ProjectView pv=ProjectView.getInstance(myProject);
DefaultMutableTreeNode rootNode=pv.getProjectViewPaneById("ProjectPane").getTreeBuilder().getRootNode();
recurseTreeAndAddFiles(rootNode);
}
private void recurseTreeAndAddFiles(@NotNull DefaultMutableTreeNode node) {
Object userObject=node.getUserObject();
if(userObject !=null && userObject instanceof PsiFileSystemItem) {
if(userObject instanceof PsiDirectory)
myProjectPaneDirs.add(((PsiFileSystemItem)userObject).getVirtualFile());
else if(userObject instanceof PsiFile)
myProjectPaneFiles.add(((PsiFileSystemItem)userObject).getVirtualFile());
}
Enumeration<DefaultMutableTreeNode> i=node.children();
while(i.hasMoreElements()) {
recurseTreeAndAddFiles(i.nextElement());
}
}
*/
private void chunkCheckAdd(Collection<VirtualFile> files) throws VcsException {
SimpleHandler ls;
ls=new SimpleHandler(myProject, myRoot, Command.LS);
ls.setRemote(true);
ls.endOptions();
Collection<List<FilePath>> splitPaths=addPaths(ls, Util.virtualFileToFilePath(new ArrayList<VirtualFile>(files)));
for(List<FilePath> paths:splitPaths)
spawnLs(paths);
}
private void checkStatusAndAddToChangeList(Collection<FilePath> paths) throws VcsException {
SimpleHandler ls=new SimpleHandler(myProject, myRoot, Command.LS);
ls.setRemote(true);
ls.endOptions();
ls.addRelativePaths(paths);
parseLsOutput(ls.run());
}
//todo wc move this into Handler
//todo wc this is NOT efficient. chunk to the right size instead.
private Collection<List<FilePath>> addPaths(Handler handler,List<FilePath> filePaths) {
Collection<List<FilePath>> returnPaths=new HashSet<List<FilePath>>();
if(handler.isAddedPathSizeTooGreat(filePaths)) {
//cut the list size in half and try again !
List<FilePath> firstHalfBigPaths=new ArrayList<FilePath>();
Iterator<FilePath> iterator=filePaths.iterator();
int i=0;
while(++i<filePaths.size()/2+1) {
firstHalfBigPaths.add(iterator.next());
//iterator.remove(); //doesn't seem to work !?
}
filePaths.removeAll(firstHalfBigPaths);
returnPaths.addAll(addPaths(handler,firstHalfBigPaths));
returnPaths.addAll(addPaths(handler,filePaths));
} else {
if(filePaths.size() > 0)
returnPaths.add(filePaths);
}
return returnPaths;
}
/**
* Collect dirty file paths
*
* @param includeChanges if true, previous changes are included in collection
* @return the set of dirty paths to check, the paths are automatically collapsed if the summary length more than limit
*/
private Collection<FilePath> dirtyPaths(boolean includeChanges) {
// TODO collapse paths with common prefix
ArrayList<FilePath> paths = new ArrayList<FilePath>();
FilePath rootPath = VcsUtil.getFilePath(myRoot.getPath(), true);
for (FilePath p : myDirtyScope.getRecursivelyDirtyDirectories()) {
addToPaths(rootPath, paths, p);
}
ArrayList<FilePath> candidatePaths = new ArrayList<FilePath>();
candidatePaths.addAll(myDirtyScope.getDirtyFilesNoExpand());
if (includeChanges) {
//todo wc figure out what the hell is going on here..
for (Change c : myChangeListManager.getChangesIn(myRoot)) {
if (c.getAfterRevision() != null) {
//noinspection ConstantConditions
addToPaths(rootPath, paths, c.getAfterRevision().getFile());
}
if (c.getBeforeRevision() != null) {
//noinspection ConstantConditions
addToPaths(rootPath, paths, c.getBeforeRevision().getFile());
}
}
}
for (FilePath p : candidatePaths) {
addToPaths(rootPath, paths, p);
}
return paths;
}
/**
* Takes the list of all directory (not .jar or .zip) roots for all modules and determines
* if any of the passed path are predecessors/ancestors of them.
* If so, adds it to the list of paths which will be returned.
* @param paths the paths to expand
* @return a list of all module source paths for which one of the passed paths is an ancestor
*/
private Collection<FilePath> expandPathsToRoots(Collection<FilePath> paths) {
Collection<FilePath> expanded=new HashSet<FilePath>();
for(Module m : ModuleManager.getInstance(myProject).getModules())
for(VirtualFile v : OrderEnumerator.orderEntries(m).getSourcePathsList().getVirtualFiles())
for(FilePath p:paths)
if(v.isDirectory() && v.getPath().startsWith(p.getPath())) //check if is directory (sometimes .zip or .jar are returned) and that it has our dirtypath as one of its ancestors
expanded.add(Util.virtualFileToFilePath(v));
return expanded;
}
/**
* Takes the list of all directory (not .jar or .zip) roots for all modules and determines
* if any of the passed path are predecessors/ancestors of them.
* If so, adds it to the list of paths which will be returned.
* @param paths the paths to expand
* @return a list of all module source paths for which one of the passed paths is an ancestor
*/
private Collection<FilePath> expandPathsToModuleBases(Collection<FilePath> paths) {
Collection<FilePath> expanded=new HashSet<FilePath>();
Set<FilePath> pathsCopy=new HashSet<FilePath>(paths);
List<VirtualFile> moduleContentRoots=new ArrayList<VirtualFile>();
for(Module m:ModuleManager.getInstance(myProject).getModules())
moduleContentRoots.addAll(Arrays.asList(ModuleRootManager.getInstance(m).getContentRoots()));
pathLoop:
for(Iterator<FilePath> pathIterator=pathsCopy.iterator();pathIterator.hasNext();) {
FilePath path=pathIterator.next();
for(Iterator<VirtualFile> mcrIterator=moduleContentRoots.iterator();mcrIterator.hasNext();) {
VirtualFile mcr=mcrIterator.next();
if(mcr.isDirectory()) {
if(mcr.getPath().startsWith(path.getPath())) { //check if is directory (sometimes .zip or .jar are returned) and that it has our dirtypath as one of its ancestors
expanded.add(Util.virtualFileToFilePath(mcr));
mcrIterator.remove(); //only add once (& don't bother iterating through this in the future)
} else if(path.getPath().startsWith(mcr.getPath())) { //or path.isUnder(mcr,false) i.e. if the path is UNDER the module, only add the path
expanded.add(path);
pathIterator.remove();
continue pathLoop;
}
}
}
}
return expanded;
}
/**
* Add path to the collection of the paths to check for this vcs root
*
* @param root the root path
* @param paths the existing paths
* @param toAdd the path to add
*/
void addToPaths(FilePath root, Collection<FilePath> paths, FilePath toAdd) {
if (VcsUtil.getVcsRootFor(myProject,toAdd) !=myRoot) {
return;
}
if (root.isUnder(toAdd, true)) {
toAdd = root;
}
for (Iterator<FilePath> i = paths.iterator(); i.hasNext();) {
FilePath p = i.next();
if (isAncestor(toAdd, p, true)) { // toAdd is an ancestor of p => adding toAdd instead of p.
i.remove();
}
if (isAncestor(p, toAdd, false)) { // p is an ancestor of toAdd => no need to add toAdd.
return;
}
}
paths.add(toAdd);
}
/**
* Returns true if childCandidate file is located under parentCandidate.
* This is an alternative to {@link com.intellij.openapi.vcs.FilePathImpl#isUnder(com.intellij.openapi.vcs.FilePath, boolean)}:
* it doesn't check VirtualFile associated with this FilePath.
* When we move a file we get a VcsDirtyScope with old and new FilePaths, but unfortunately the virtual file in the FilePath is
* refreshed ({@link com.intellij.openapi.vcs.changes.VirtualFileHolder#cleanAndAdjustScope(com.intellij.openapi.vcs.changes.VcsModifiableDirtyScope)}
* and thus points to the new position which makes FilePathImpl#isUnder useless.
*
* @param parentCandidate FilePath which we check to be the parent of childCandidate.
* @param childCandidate FilePath which we check to be a child of parentCandidate.
* @param strict if false, the method also returns true if files are equal
* @return true if childCandidate is a child of parentCandidate.
*/
private static boolean isAncestor(FilePath parentCandidate, FilePath childCandidate, boolean strict) {
return FileUtil.isAncestor(parentCandidate.getIOFile(), childCandidate.getIOFile(), strict);
}
/**
* Collect all changes
*
* @throws VcsException if there is a problem with running
*/
private void collectVcsModifiedList() throws VcsException {
Collection<FilePath> dirtyPaths = dirtyPaths(true);
if (dirtyPaths.isEmpty()) {
return;
}
// HandlerUtil.runInCurrentThread(ls, myProgressIndicator, false, "VCS refresh");
// HandlerUtil.doSynchronously(ls, "VCS refresh", "VCS refresh");
SimpleHandler lsco= new SimpleHandler(myProject, myRoot, Command.LS_CHECKOUTS);
lsco.setRemote(true);
//lsco.setSilent(true);
//lsco.setStdoutSuppressed(true);
lsco.addParameters("-a -me -cvi");
lsco.endOptions();
parseLsCheckoutsOutput(lsco.run());
}
private @NotNull Set<String> getFsWritableFiles() throws VcsException {
Collection<FilePath> dirtyPaths = dirtyPaths(true);
Set<String> writableFiles=new HashSet<String>();
for(int i=0; i<MAX_THREADS; i++) {
myRecurseThreads.add(new RecurseRunnable(writableFiles));
}
//testSetup();
for(FilePath path:expandPathsToModuleBases(dirtyPaths))
spawnOrRecurse(path.getIOFile(),writableFiles, -1);
VirtualFile projBaseDir=myProject.getBaseDir();
if(projBaseDir!=null && isInRoot(projBaseDir))
spawnOrRecurse(Util.virtualFileToFile(projBaseDir),writableFiles,1); //recurse only the base directory, no children
synchronized(myRecurseThreads) {
while(true) { //wait until all threads have exited
if(myRecurseThreads.size()==MAX_THREADS)
break;
try {
myRecurseThreads.wait();
} catch(InterruptedException ignored) {}
}
}
myRecurseThreads.clear();
return writableFiles;
}
private VirtualFile createFileIfInRoot(String filename) throws VcsException {
VirtualFile file=getVirtualFile(filename);
return isInRoot(file) ? file : null;
}
private VirtualFile getVirtualFile(String filename) throws VcsException {
VirtualFile file=myRoot.findFileByRelativePath(Util.unescapePath(filename));
if(file==null)
file=myRoot.findFileByRelativePath(
Util.unescapePath(
Util.relativePath(myRoot,VcsUtil.getFilePath(filename))));
return file;
}
private boolean isInRoot(VirtualFile file) throws VcsException {
return VcsUtil.getVcsRootFor(myProject,file) ==myRoot;
}
private boolean addFileToListIfExistsAndInRoot(List<VirtualFile> list, String filename) {
boolean failed=false;
VirtualFile file=null;
try {
file=createFileIfInRoot(filename);
} catch(VcsException e) {
failed=true;
}
if(file!=null && !failed) {
list.add(file);
return true;
} else {
return false;
}
}
/**
* Parse Command.LS_CHECKOUTS output.
* @param list the output from the lsco command
* @throws VcsException in several cases
*/
private void parseLsCheckoutsOutput(String list) throws VcsException {
//Line format:
//--12-21T17:09 ascher checkout version "C:\cc\baplugintest\serverdev\lost+found\wrapper_32.6223c92a584d4266bcc1b081259b9b2c" from \main\0 (reserved)
//--02-25T18:12 ascher checkout directory version "C:\cc\bamain\serverdev\server\mailgtw\mmt\cache\logic\_src\com\oz\mailgtw\mmt\cache\logic\billing\factory" from \main\2 (unreserved)
BufferedReader reader=new BufferedReader(new StringReader(list));
//final String filenameStartToken="checkout version \"";
final String filenameStartToken="checkout (directory )?version \"";
final String filenameEndToken="\" from ";
while(true) {
String line;
try {
line=reader.readLine();
} catch(IOException e) {
log.error(e);
break;
}
if(line==null) {
break;
} else {
String[] splitOnToken=line.split(filenameStartToken);
if(splitOnToken.length==2) {
int filenameEnd=splitOnToken[1].lastIndexOf(filenameEndToken);
if(filenameEnd>0) {
String filename=splitOnToken[1].substring(0,filenameEnd);
File file=new File(filename);
String[] parts=splitOnToken[1].substring(filename.length()+filenameEndToken.length(),splitOnToken[1].length()).split("\\s+",0);
String relativeFilename=Util.relativePath(myRoot,file);
VirtualFile vfile=getVirtualFile(filename);
if(vfile !=null && vfile.exists() && isInRoot(vfile)) {
//this is a checked out file, which we'll automatically consider to be "modified"
//in this case, the next string after "from" should be the version number that the checkout came from
com.intellij.openapi.vcs.changes.ContentRevision before=
ContentRevision.createRevision(myRoot,
relativeFilename,
HistoryUtils.createUnvalidatedRevisionNumber(parts[0]),
myProject,
false,
true);
com.intellij.openapi.vcs.changes.ContentRevision after=
ContentRevision.createRevision(myRoot,
relativeFilename,
null,
myProject,
false,
true);
if(!file.isDirectory() || VcsSettings.getInstance(myProject).isShowDirectories())
myChanges.add(new Change(before, after, FileStatus.MODIFIED));
//else //appears in yellow as file to be added
// myChanges.add(new Change(before, after, FileStatus.IGNORED));
} else { //it's a checked-out file that's been deleted
//todo wc if the file is deleted but isn't in the root, do not add to list of changes
com.intellij.openapi.vcs.changes.ContentRevision before=
ContentRevision.createRevision(myRoot,
relativeFilename,
HistoryUtils.createUnvalidatedRevisionNumber(parts[0]),
myProject,
false,
true);
com.intellij.openapi.vcs.changes.ContentRevision after=
ContentRevision.createRevision(myRoot,
relativeFilename,
null,
myProject,
false,
true);
//todo wc if it's a deleted file, we won't actually know if it's a directory or not so it will still be shown.
if(!file.isDirectory() || VcsSettings.getInstance(myProject).isShowDirectories())
myChanges.add(new Change(before, after, FileStatus.DELETED));
//else
// myChanges.add(new Change(before, after, FileStatus.IGNORED));
}
}
}
}
}
}
/**
* Parse Command.LS output.
* @param list the output from the ls command
* @throws VcsException in several cases
*/
private void parseLsOutput(String list) throws VcsException {
//Line format:
// wrapper_32.6223c92a584d4266bcc1b081259b9b2c@@\main\CHECKEDOUT from \main\0 Rule: CHECKEDOUT //modified
// wrapper_diameter_loopback.conf.375035980431452ba39e23f44acd7567@@\main\0 [hijacked] Rule: \main\LATEST //modified
// lost+found.iml //unversioned (added)
// wrapper_yahooservices_out.conf.72bc32ce8b4a417b9961dda95d7799bf@@\main\0 [not loaded] Rule: \main\LATEST //(ignored)
// rapper_yahooservices_out.conf.72bc32ce8b4a417b9961dda95d7799bf@@\main\0 [loaded but missing] Rule: \main\LATEST //deleted
BufferedReader reader=new BufferedReader(new StringReader(list));
String line;
int versionStartIndex; //the position of the start of the version number in the pname (pname is file+version)
String filename;
while(true) {
//noinspection UnusedAssignment
line=null;
//noinspection UnusedAssignment
versionStartIndex=-2; //bogus value which will be overwritten
//noinspection UnusedAssignment
filename=null;
try {
line=reader.readLine();
} catch(IOException e) {
log.error(e);
break;
}
//a lot of ifs and if-else but I was hoping the flow would be easier to follow than having 'continue' statements everywhere (it may not be)
if(line==null) {
break;
} else {
//look for the symbols that denote the start of the version number
versionStartIndex=line.lastIndexOf(VERSION_KEY); //search for ver # from the back in case filename contains @@
if(versionStartIndex==-1) {
//the version was not found, so the line looks like this:
//lost+found.iml
//this is what unversioned files look like. Add it to the unversioned list...
filename=line;
addFileToListIfExistsAndInRoot(myUnversioned,filename);
} else {
if(versionStartIndex > 0) {//basic sanity
//we did find the version tag. Our line looks like one of these:
//file1.ext@@\main\CHECKEDOUT from \main\0 Rule: CHECKEDOUT //modified
//file2.ext@@\main\0 [hijacked] Rule: \main\LATEST //modified
//file3.ext@@\main\0 [loaded but missing] Rule: \main\LATEST //deleted
//file4.ext@@\main\0 [not loaded] Rule: \main\LATEST //(ignored)
//file5.ext //unversioned (added)
filename=line.substring(0,versionStartIndex); //copy the file name
//now split everything beyond the version marking at each whitespace
String[] parts=line.substring(versionStartIndex+VERSION_KEY.length(), line.length()).split("\\s+",0);
if(parts.length >= 3) { //the version, the status/checkout version and the Rule at minimum, longer in some cases
String version=parts[0]; //copy the version
VirtualFile file=createFileIfInRoot(filename);
if(file != null) {
String relativeFilename=Util.relativePath(myRoot,file);
if(parts[1].equals("from")) {
//this is a checked out file, which we'll automatically consider to be "modified"
//in this case, the next string after "from" should be the version number that the checkout came from
com.intellij.openapi.vcs.changes.ContentRevision before=ContentRevision.createRevision(myRoot,relativeFilename,HistoryUtils.createUnvalidatedRevisionNumber(parts[2]),myProject,false,true);
//com.intellij.openapi.vcs.changes.ContentRevision after=ContentRevision.createRevision(myRoot, relativeFilename, new VcsRevisionNumber(version), myProject, false, true);
com.intellij.openapi.vcs.changes.ContentRevision after=ContentRevision.createRevision(myRoot,relativeFilename,null,myProject,false,true);
myChanges.add(new Change(before, after, FileStatus.MODIFIED));
} else {
if(parts[1].equals("[hijacked]")) {
//wrapper_diameter_loopback.conf.375035980431452ba39e23f44acd7567@@\main\0 [hijacked] Rule: \main\LATEST
com.intellij.openapi.vcs.changes.ContentRevision before=ContentRevision.createRevision(myRoot, relativeFilename,HistoryUtils.createUnvalidatedRevisionNumber(version), myProject, false, true);
com.intellij.openapi.vcs.changes.ContentRevision after=ContentRevision.createRevision(myRoot, relativeFilename, null, myProject, false, true);
myChanges.add(new Change(before, after, FileStatus.HIJACKED));
}
}
} else {
if(parts.length >= 4 && parts[1].equals("[loaded") && parts[2].equals("but") && parts[3].equals("missing]")) {
//wrapper_yahooservices_out.conf.72bc32ce8b4a417b9961dda95d7799bf@@\main\0 [not loaded] Rule: \main\LATEST
com.intellij.openapi.vcs.changes.ContentRevision before=ContentRevision.createRevision(myRoot, filename,HistoryUtils.createUnvalidatedRevisionNumber(version), myProject, false, true);
com.intellij.openapi.vcs.changes.ContentRevision after=ContentRevision.createRevision(myRoot, filename, null, myProject, true, true);
myChanges.add(new Change(before, after, FileStatus.DELETED_FROM_FS));
} else {
//default/ignored.
//if(parts[1].equals("[not") && parts[2].equals("loaded]")) {} //ignored
}
}
}
}
}
}
}
}
/*
//for testing methods of ignoring files
Set<VirtualFile> myTestFiles = new HashSet<VirtualFile>();
private void testSetup() {
addChildren(new File("C:\\cc\\bamain\\serverdev\\server"),false,false);
addChildren(new File("C:\\cc\\bamain\\serverdev\\server"),true,false);
addChildren(new File("C:\\cc\\bamain\\serverdev\\server\\mailgtw"),true,false);
}
private void addChildren(File file,boolean children,boolean onlyDirectories) {
if(children)
for(File f:file.listFiles())
addFile(f,onlyDirectories);
else
addFile(file,onlyDirectories);
}
private void addFile(File file,boolean onlyDirectory) {
try {
VirtualFile v=Util.fileToVirtualFile(myRoot,file,true);
if(v!=null && (!onlyDirectory || v.isDirectory()) )
myTestFiles.add(v);
} catch(VcsException e) {}
}
*/
private void recurseHijackedFiles(File file, Set<String> writableFiles, int maxDepth) throws VcsException {
//todo wc BEWARE LINKS THAT WILL CAUSE INFINITE RECURSION - OH NOES!
VirtualFile vf=Util.stringToVirtualFile(myRoot,Util.relativePath(myRoot,file),true);
if(vf!=null) {
//skip excluded files
/* these are always all false
//for testing ways of finding excluded files
//if(myTestFiles.contains(vf)) {
if(maxDepth==0) {
System.out.println(vf);
System.out.println("\tprojectContainsFile(proj,file,islib=false) \t"+ModuleUtil.projectContainsFile(myProject, vf, false));
System.out.println("\tprojectContainsFile(proj,file,islib=true) \t"+ModuleUtil.projectContainsFile(myProject,vf,true));
System.out.println("\tisProjectExcludeRoot \t\t"+DirectoryIndex.getInstance(myProject).isProjectExcludeRoot(vf));
System.out.println("\tmyFileIndex.isIgnored \t\t"+myFileIndex.isIgnored(vf));
System.out.println("\tChangeListManager.isIgnoredFile \t\t"+ChangeListManager.getInstance(myProject).isIgnoredFile(vf));
}
*/
//if(/*ModuleUtil.projectContainsFile(myProject,vf,false) &&
if(!ChangeListManager.getInstance(myProject).isIgnoredFile(vf)
&& !myFileIndex.isIgnored(vf)
&& (myPathFilter==null
|| !myPathFilter.matcher(vf.getPath()).find() )) { //if this line is removed, we must find a way to ignore .keep and .contrib
//if(file.isDirectory()) { //not needed, implicit check below
File[] children=file.listFiles();
if(children!=null) { //implicit directory AND IO error check.
if(maxDepth!=0)
for(File child:children)
spawnOrRecurse(child, writableFiles, maxDepth>0? --maxDepth:maxDepth); //if maxDepth is negative, don't subtract first.
} else { //is a file
//check if it's read-only
//if yes, skip
//if no, add to dirty list or add to changes right away
if(file.canWrite() && vf.getExtension()!=null) {
String relativeFilename=Util.relativePath(myRoot,file);
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized(writableFiles) {
writableFiles.add(relativeFilename); //we don't know if it's been added or hijacked so don't put it in the change list yet, just take note
}
}
}
}
} else { //probably a deleted file.
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized(writableFiles) { //we'll do our best to keep track of it...
writableFiles.add(Util.relativePath(myRoot,file)); //put it in the writeable files to be verified
}
}
}
private void spawnOrRecurse(File file, Set<String> writableFiles, int maxDepth) throws VcsException {
RecurseRunnable runner=null;
synchronized(myRecurseThreads) {
if(!myRecurseThreads.isEmpty())
runner=myRecurseThreads.remove(myRecurseThreads.size()-1); //remove the last element instead of the first so that we don't have to recopy the array
}
if(runner == null) //there are no threads left.
recurseHijackedFiles(file, writableFiles, maxDepth);
else {
runner.setFile(file);
runner.setMaxDepth(maxDepth);
new Thread(runner).start(); //runner.run();
}
}
private void endThreadRun(RecurseRunnable runner) {
synchronized(myRecurseThreads) {
myRecurseThreads.add(runner);
myRecurseThreads.notify();
}
}
private void endThreadRun(LsRunnable runner) {
synchronized(myLsThreads) {
myLsThreads.add(runner);
myLsThreads.notify();
}
}
private void spawnLs(Collection<FilePath> files) throws VcsException {
LsRunnable runner=null;
synchronized(myLsThreads) {
if(!myLsThreads.isEmpty())
runner=myLsThreads.remove(myLsThreads.size()-1); //remove the last element instead of the first so that we don't have to recopy the array
}
if(runner == null) //there are no threads left.
checkStatusAndAddToChangeList(files);
else {
runner.setFiles(files);
new Thread(runner).start(); //runner.run();
}
}
private class RecurseRunnable implements Runnable {
//Set<File> myIterableFiles;
File myFile;
Set<String> myWritableFiles;
private int myMaxDepth=0;
public RecurseRunnable(Set<String> writableFiles) {
//myIterableFiles=new HashSet<File>();
//myIterableFiles.add(file);
myFile=null;
myWritableFiles=writableFiles;
}
@Override
public void run() {
if(myFile == null) {
throw new IllegalStateException("This object's file must be set prior to running");
}
try {
recurseHijackedFiles(myFile,myWritableFiles,myMaxDepth);
} catch(VcsException e) {
popupNotification(e);
}
myFile=null;
endThreadRun(this);
}
public void setFile(File file) {
myFile=file;
}
public void setMaxDepth(int maxDepth) {
myMaxDepth=maxDepth;
}
}
private class LsRunnable implements Runnable {
Collection<FilePath> myFiles;
public void setFiles(Collection<FilePath> files) {
myFiles=files;
}
@Override
public void run() {
if(myFiles == null) {
throw new IllegalStateException("This object's file must be set prior to running");
}
try {
checkStatusAndAddToChangeList(myFiles);
} catch(VcsException e) {
popupNotification(e);
}
myFiles=null;
endThreadRun(this);
}
}
private void popupNotification(Throwable e) {
String message=Bundle.message("changes.err.content")+e.getMessage();
//popupNotification(NotificationType.ERROR,message);
log.error(message,e);
}
private void popupNotification(NotificationType type,String s) {
Notifications.Bus.notify(
new Notification(Vcs.NOTIFICATION_GROUP_ID,Bundle.message("changes.err.title"),s,type),
myProject);
}
}