0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041:
0042: package org.netbeans.modules.subversion;
0043:
0044: import java.io.File;
0045: import java.util.regex.*;
0046: import org.netbeans.modules.versioning.util.ListenersSupport;
0047: import org.netbeans.modules.versioning.util.VersioningListener;
0048: import org.netbeans.modules.versioning.spi.VersioningSupport;
0049: import org.netbeans.modules.subversion.util.Context;
0050: import org.netbeans.modules.subversion.util.SvnUtils;
0051: import org.netbeans.modules.turbo.Turbo;
0052: import org.netbeans.modules.turbo.CustomProviders;
0053: import org.openide.filesystems.FileUtil;
0054: import org.openide.filesystems.FileObject;
0055: import org.openide.filesystems.FileStateInvalidException;
0056: import java.util.*;
0057: import org.netbeans.modules.subversion.client.SvnClient;
0058: import org.netbeans.modules.subversion.client.SvnClientExceptionHandler;
0059: import org.netbeans.modules.versioning.util.Utils;
0060: import org.openide.filesystems.FileSystem;
0061: import org.openide.util.RequestProcessor;
0062: import org.tigris.subversion.svnclientadapter.*;
0063: import org.tigris.subversion.svnclientadapter.ISVNStatus;
0064:
0065: /**
0066: * Central part of Subversion status management, deduces and caches statuses of files under version control.
0067: *
0068: * @author Maros Sandor
0069: */
0070: public class FileStatusCache implements ISVNNotifyListener {
0071:
0072: /**
0073: * Indicates that status of a file changed and listeners SHOULD check new status
0074: * values if they are interested in this file.
0075: * First parameter: File whose status changes
0076: * Second parameter: old FileInformation object, may be null
0077: * Third parameter: new FileInformation object
0078: */
0079: public static final Object EVENT_FILE_STATUS_CHANGED = new Object();
0080:
0081: /**
0082: * A special map saying that no file inside the folder is managed.
0083: */
0084: private static final Map<File, FileInformation> NOT_MANAGED_MAP = new NotManagedMap();
0085:
0086: public static final ISVNStatus REPOSITORY_STATUS_UNKNOWN = null;
0087:
0088: // Constant FileInformation objects that can be safely reused
0089: // Files that have a revision number cannot share FileInformation objects
0090: private static final FileInformation FILE_INFORMATION_EXCLUDED = new FileInformation(
0091: FileInformation.STATUS_NOTVERSIONED_EXCLUDED, false);
0092: private static final FileInformation FILE_INFORMATION_EXCLUDED_DIRECTORY = new FileInformation(
0093: FileInformation.STATUS_NOTVERSIONED_EXCLUDED, true);
0094: private static final FileInformation FILE_INFORMATION_UPTODATE_DIRECTORY = new FileInformation(
0095: FileInformation.STATUS_VERSIONED_UPTODATE, true);
0096: private static final FileInformation FILE_INFORMATION_NOTMANAGED = new FileInformation(
0097: FileInformation.STATUS_NOTVERSIONED_NOTMANAGED, false);
0098: private static final FileInformation FILE_INFORMATION_NOTMANAGED_DIRECTORY = new FileInformation(
0099: FileInformation.STATUS_NOTVERSIONED_NOTMANAGED, true);
0100: private static final FileInformation FILE_INFORMATION_UNKNOWN = new FileInformation(
0101: FileInformation.STATUS_UNKNOWN, false);
0102:
0103: /**
0104: * Auxiliary conflict file siblings
0105: * After update: *.r#, *.mine
0106: * After merge: *.working, *.merge-right.r#, *.metge-left.r#
0107: */
0108: private static final Pattern auxConflictPattern = Pattern
0109: .compile("(.*?)\\.((r\\d+)|(mine)|" + // NOI18N
0110: "(working)|(merge-right\\.r\\d+)|((merge-left.r\\d+)))$"); // NOI18N
0111:
0112: /*
0113: * Holds three kinds of information: what folders we have scanned, what files we have found
0114: * and what statuses of these files are.
0115: * If a directory is not found as a key in the map, we have not scanned it yet.
0116: * If it has been scanned, it maps to a Set of files that were found somehow out of sync with the
0117: * repository (have any other status then up-to-date). In case all files are up-to-date, it maps
0118: * to Collections.EMPTY_MAP. Entries in this map are created as directories are scanne, are never removed and
0119: * are updated by the refresh method.
0120: */
0121:
0122: private final Turbo turbo;
0123:
0124: /**
0125: * Identifies attribute that holds information about all non STATUS_VERSIONED_UPTODATE files.
0126: *
0127: * <p>Key type: File identifying a folder
0128: * <p>Value type: Map<File, FileInformation>
0129: */
0130: private final String FILE_STATUS_MAP = DiskMapTurboProvider.ATTR_STATUS_MAP;
0131:
0132: private DiskMapTurboProvider cacheProvider;
0133:
0134: private Subversion svn;
0135:
0136: private Set<FileSystem> filesystemsToRefresh;
0137:
0138: private RequestProcessor.Task refreshFilesystemsTask;
0139:
0140: FileStatusCache() {
0141: this .svn = Subversion.getInstance();
0142: cacheProvider = new DiskMapTurboProvider();
0143: turbo = Turbo.createCustom(new CustomProviders() {
0144: private final Set providers = Collections
0145: .singleton(cacheProvider);
0146:
0147: public Iterator providers() {
0148: return providers.iterator();
0149: }
0150: }, 200, 5000);
0151: }
0152:
0153: // --- Public interface -------------------------------------------------
0154:
0155: /**
0156: * Lists <b>modified files</b> and all folders that are known to be inside
0157: * this folder. There are locally modified files present
0158: * plus any files that exist in the folder in the remote repository. It
0159: * returns all folders, including CVS folders.
0160: *
0161: * @param dir folder to list
0162: * @return
0163: */
0164: public File[] listFiles(File dir) {
0165: Set<File> files = getScannedFiles(dir).keySet();
0166: return files.toArray(new File[files.size()]);
0167: }
0168:
0169: /**
0170: * Lists <b>interesting files</b> that are known to be inside given folders.
0171: * These are locally and remotely modified and ignored files.
0172: *
0173: * <p>Comapring to CVS this method returns both folders and files.
0174: *
0175: * @param context context to examine
0176: * @param includeStatus limit returned files to those having one of supplied statuses
0177: * @return File [] array of interesting files
0178: */
0179: public File[] listFiles(Context context, int includeStatus) {
0180: Set<File> set = new HashSet<File>();
0181: Map allFiles = cacheProvider.getAllModifiedValues();
0182: for (Iterator i = allFiles.keySet().iterator(); i.hasNext();) {
0183: File file = (File) i.next();
0184: FileInformation info = (FileInformation) allFiles.get(file);
0185: if ((info.getStatus() & includeStatus) == 0)
0186: continue;
0187: File[] roots = context.getRootFiles();
0188: for (int j = 0; j < roots.length; j++) {
0189: File root = roots[j];
0190: if (VersioningSupport.isFlat(root)) {
0191: if (file.equals(root)
0192: || file.getParentFile().equals(root)) {
0193: set.add(file);
0194: break;
0195: }
0196: } else {
0197: if (SvnUtils.isParentOrEqual(root, file)) {
0198: set.add(file);
0199: break;
0200: }
0201: }
0202: }
0203: }
0204: if (context.getExclusions().size() > 0) {
0205: for (Iterator i = context.getExclusions().iterator(); i
0206: .hasNext();) {
0207: File excluded = (File) i.next();
0208: for (Iterator j = set.iterator(); j.hasNext();) {
0209: File file = (File) j.next();
0210: if (SvnUtils.isParentOrEqual(excluded, file)) {
0211: j.remove();
0212: }
0213: }
0214: }
0215: }
0216: return set.toArray(new File[set.size()]);
0217: }
0218:
0219: /**
0220: * Lists <b>interesting files</b> that are known to be inside given folders.
0221: * These are locally and remotely modified and ignored files.
0222: *
0223: * <p>Comapring to CVS this method returns both folders and files.
0224: *
0225: * @param roots context to examine
0226: * @param includeStatus limit returned files to those having one of supplied statuses
0227: * @return File [] array of interesting files
0228: */
0229: public File[] listFiles(File[] roots, int includeStatus) {
0230: Set<File> set = new HashSet<File>();
0231: Map allFiles = cacheProvider.getAllModifiedValues();
0232: for (Iterator i = allFiles.keySet().iterator(); i.hasNext();) {
0233: File file = (File) i.next();
0234: FileInformation info = (FileInformation) allFiles.get(file);
0235: if ((info.getStatus() & includeStatus) == 0)
0236: continue;
0237: for (int j = 0; j < roots.length; j++) {
0238: File root = roots[j];
0239: if (VersioningSupport.isFlat(root)) {
0240: if (file.getParentFile().equals(root)) {
0241: set.add(file);
0242: break;
0243: }
0244: } else {
0245: if (SvnUtils.isParentOrEqual(root, file)) {
0246: set.add(file);
0247: break;
0248: }
0249: }
0250: }
0251: }
0252: return set.toArray(new File[set.size()]);
0253: }
0254:
0255: /**
0256: * Determines the versioning status of a file. This method accesses disk and may block for a long period of time.
0257: *
0258: * @param file file to get status for
0259: * @return FileInformation structure containing the file status
0260: * @see FileInformation
0261: */
0262: public FileInformation getStatus(File file) {
0263: if (svn.isAdministrative(file))
0264: return FILE_INFORMATION_NOTMANAGED_DIRECTORY;
0265: File dir = file.getParentFile();
0266: if (dir == null) {
0267: return FILE_INFORMATION_NOTMANAGED; //default for filesystem roots
0268: }
0269: Map files = getScannedFiles(dir);
0270: if (files == NOT_MANAGED_MAP)
0271: return FILE_INFORMATION_NOTMANAGED;
0272: FileInformation fi = (FileInformation) files.get(file);
0273: if (fi != null) {
0274: return fi;
0275: }
0276: if (!exists(file))
0277: return FILE_INFORMATION_UNKNOWN;
0278: if (file.isDirectory()) {
0279: return refresh(file, REPOSITORY_STATUS_UNKNOWN);
0280: } else {
0281: return new FileInformation(
0282: FileInformation.STATUS_VERSIONED_UPTODATE, false);
0283: }
0284: }
0285:
0286: /**
0287: * Looks up cacnehed file status.
0288: *
0289: * @param file file to check
0290: * @return give file's status or null if the file's status is not in cache
0291: */
0292: FileInformation getCachedStatus(File file) {
0293: File parent = file.getParentFile();
0294: if (parent == null)
0295: return FILE_INFORMATION_NOTMANAGED_DIRECTORY;
0296: Map<File, FileInformation> files = (Map<File, FileInformation>) turbo
0297: .readEntry(parent, FILE_STATUS_MAP);
0298: return files != null ? files.get(file) : null;
0299: }
0300:
0301: private FileInformation refresh(File file,
0302: ISVNStatus repositoryStatus, boolean forceChangeEvent) {
0303:
0304: boolean refreshDone = false;
0305: FileInformation current = null;
0306: FileInformation fi = null;
0307: File[] content = null;
0308:
0309: synchronized (this ) {
0310: File dir = file.getParentFile();
0311: if (dir == null) {
0312: return FILE_INFORMATION_NOTMANAGED; //default for filesystem roots
0313: }
0314: Map<File, FileInformation> files = getScannedFiles(dir);
0315: if (files == NOT_MANAGED_MAP
0316: && repositoryStatus == REPOSITORY_STATUS_UNKNOWN)
0317: return FILE_INFORMATION_NOTMANAGED;
0318: current = files.get(file);
0319:
0320: ISVNStatus status = null;
0321: try {
0322: SvnClient client = Subversion.getInstance().getClient(
0323: false);
0324: status = client.getSingleStatus(file);
0325: if (status != null
0326: && SVNStatusKind.UNVERSIONED.equals(status
0327: .getTextStatus())) {
0328: status = null;
0329: }
0330: } catch (SVNClientException e) {
0331: // svnClientAdapter does not return SVNStatusKind.UNVERSIONED!!!
0332: // unversioned resource is expected getSingleStatus()
0333: // does not return SVNStatusKind.UNVERSIONED but throws exception instead
0334: // instead of throwing exception
0335: if (SvnClientExceptionHandler.isUnversionedResource(e
0336: .getMessage()) == false) {
0337: // missing or damaged entries
0338: // or ignored file
0339: SvnClientExceptionHandler.notifyException(e, false,
0340: false);
0341: }
0342: }
0343: fi = createFileInformation(file, status, repositoryStatus);
0344: if (equivalent(fi, current)) {
0345: refreshDone = true;
0346: }
0347: // do not include uptodate files into cache, missing directories must be included
0348: if (!refreshDone
0349: && current == null
0350: && !fi.isDirectory()
0351: && fi.getStatus() == FileInformation.STATUS_VERSIONED_UPTODATE) {
0352: refreshDone = true;
0353: }
0354:
0355: if (!refreshDone) {
0356: if (fi.getStatus() == FileInformation.STATUS_UNKNOWN
0357: && current != null
0358: && current.isDirectory()
0359: && (current.getStatus() == FileInformation.STATUS_VERSIONED_DELETEDLOCALLY || current
0360: .getStatus() == FileInformation.STATUS_VERSIONED_REMOVEDLOCALLY)) {
0361: // - if the file was deleted then all it's children have to be refreshed.
0362: // - we have to list the children before the turbo.writeEntry() call
0363: // as that unfortunatelly tends to purge them from the cache
0364: content = listFiles(new File[] { file }, ~0);
0365: }
0366:
0367: file = FileUtil.normalizeFile(file);
0368: dir = FileUtil.normalizeFile(dir);
0369: Map<File, FileInformation> newFiles = new HashMap<File, FileInformation>(
0370: files);
0371: if (fi.getStatus() == FileInformation.STATUS_UNKNOWN) {
0372: newFiles.remove(file);
0373: turbo.writeEntry(file, FILE_STATUS_MAP, null); // remove mapping in case of directories
0374: } else if (fi.getStatus() == FileInformation.STATUS_VERSIONED_UPTODATE
0375: && file.isFile()) {
0376: newFiles.remove(file);
0377: } else {
0378: newFiles.put(file, fi);
0379: }
0380: assert newFiles.containsKey(dir) == false;
0381: turbo.writeEntry(dir, FILE_STATUS_MAP,
0382: newFiles.size() == 0 ? null : newFiles);
0383: }
0384: }
0385:
0386: if (!refreshDone) {
0387: if (content == null && file.isDirectory()
0388: && needRecursiveRefresh(fi, current)) {
0389: content = listFiles(file);
0390: }
0391:
0392: if (content != null) {
0393: for (int i = 0; i < content.length; i++) {
0394: refresh(content[i], REPOSITORY_STATUS_UNKNOWN);
0395: }
0396: }
0397: fireFileStatusChanged(file, current, fi);
0398: } else if (forceChangeEvent) {
0399: fireFileStatusChanged(file, current, fi);
0400: }
0401: return fi;
0402: }
0403:
0404: /**
0405: * Refreshes the status of the file given the repository status. Repository status is filled
0406: * in when this method is called while processing server output.
0407: *
0408: * <p>Note: it's not necessary if you use Subversion.getClient(), it
0409: * updates the cache automatically using onNotify(). It's not
0410: * fully reliable for removed files.
0411: *
0412: * @param file
0413: * @param repositoryStatus
0414: */
0415: public FileInformation refresh(File file,
0416: ISVNStatus repositoryStatus) {
0417: return refresh(file, repositoryStatus, false);
0418: }
0419:
0420: /**
0421: * Two FileInformation objects are equivalent if their status contants are equal AND they both reperesent a file (or
0422: * both represent a directory) AND Entries they cache, if they can be compared, are equal.
0423: *
0424: * @param other object to compare to
0425: * @return true if status constants of both object are equal, false otherwise
0426: */
0427: private static boolean equivalent(FileInformation main,
0428: FileInformation other) {
0429: if (other == null || main.getStatus() != other.getStatus()
0430: || main.isDirectory() != other.isDirectory())
0431: return false;
0432:
0433: ISVNStatus e1 = main.getEntry(null);
0434: ISVNStatus e2 = other.getEntry(null);
0435: return e1 == e2 || e1 == null || e2 == null || equal(e1, e2);
0436: }
0437:
0438: /**
0439: * Replacement for missing Entry.equals(). It is implemented as a separate method to maintain compatibility.
0440: *
0441: * @param e1 first entry to compare
0442: * @param e2 second Entry to compare
0443: * @return true if supplied entries contain equivalent information
0444: */
0445: private static boolean equal(ISVNStatus e1, ISVNStatus e2) {
0446: long r1 = -1;
0447: if (e1 != null) {
0448: SVNRevision r = e1.getRevision();
0449: r1 = r != null ? e1.getRevision().getNumber() : r1;
0450: }
0451:
0452: long r2 = -2;
0453: if (e2 != null) {
0454: SVNRevision r = e1.getRevision();
0455: r2 = r != null ? e2.getRevision().getNumber() : r2;
0456: }
0457:
0458: if (r1 != r2) {
0459: return false;
0460: }
0461: return e1.getUrl() == e2.getUrl() || e1.getUrl() != null
0462: && e1.getUrl().equals(e2.getUrl());
0463: }
0464:
0465: private boolean needRecursiveRefresh(FileInformation fi,
0466: FileInformation current) {
0467: // XXX review this part and see also the places where svnutils.refreshrecursively is called.
0468: // looks like the same thing is done at diferent places in a different way but the same result.
0469: if (fi.getStatus() == FileInformation.STATUS_NOTVERSIONED_EXCLUDED
0470: || current != null
0471: && current.getStatus() == FileInformation.STATUS_NOTVERSIONED_EXCLUDED)
0472: return true;
0473: if (fi.getStatus() == FileInformation.STATUS_NOTVERSIONED_NOTMANAGED
0474: || current != null
0475: && current.getStatus() == FileInformation.STATUS_NOTVERSIONED_NOTMANAGED)
0476: return true;
0477: if (fi.getStatus() == FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY
0478: || current != null
0479: && current.getStatus() == FileInformation.STATUS_VERSIONED_ADDEDLOCALLY)
0480: return true;
0481: return false;
0482: }
0483:
0484: /**
0485: * Refreshes information about a given file or directory ONLY if its status is already cached. The
0486: * only exception are non-existing files (new-in-repository) whose statuses are cached in all cases.
0487: *
0488: * @param file
0489: * @param repositoryStatus
0490: */
0491: public void refreshCached(File file, ISVNStatus repositoryStatus) {
0492: refresh(file, repositoryStatus);
0493: }
0494:
0495: /**
0496: * Refreshes status of all files inside given context. Files that have some remote status, eg. REMOTELY_ADDED
0497: * are brought back to UPTODATE.
0498: *
0499: * @param ctx context to refresh
0500: */
0501: public void refreshCached(Context ctx) {
0502:
0503: File[] files = listFiles(ctx, ~0);
0504:
0505: for (int i = 0; i < files.length; i++) {
0506: File file = files[i];
0507: refreshCached(file, REPOSITORY_STATUS_UNKNOWN);
0508: }
0509: }
0510:
0511: // --- Package private contract ------------------------------------------
0512:
0513: Map<File, FileInformation> getAllModifiedFiles() {
0514: return cacheProvider.getAllModifiedValues();
0515: }
0516:
0517: /**
0518: * Refreshes given directory and all subdirectories.
0519: *
0520: * @param dir directory to refresh
0521: */
0522: void directoryContentChanged(File dir) {
0523: Map originalFiles = (Map) turbo.readEntry(dir, FILE_STATUS_MAP);
0524: if (originalFiles != null) {
0525: for (Iterator i = originalFiles.keySet().iterator(); i
0526: .hasNext();) {
0527: File file = (File) i.next();
0528: refresh(file, REPOSITORY_STATUS_UNKNOWN);
0529: }
0530: }
0531: }
0532:
0533: /**
0534: * Cleans up the cache by removing or correcting entries that are no longer valid or correct.
0535: */
0536: void cleanUp() {
0537: Map files = cacheProvider.getAllModifiedValues();
0538: for (Iterator i = files.keySet().iterator(); i.hasNext();) {
0539: File file = (File) i.next();
0540: FileInformation info = (FileInformation) files.get(file);
0541: if ((info.getStatus() & FileInformation.STATUS_LOCAL_CHANGE) != 0) {
0542: refresh(file, REPOSITORY_STATUS_UNKNOWN);
0543: } else if (info.getStatus() == FileInformation.STATUS_NOTVERSIONED_EXCLUDED) {
0544: // remove entries that were excluded but no longer exist
0545: // cannot simply call refresh on excluded files because of 'excluded on server' status
0546: if (!exists(file)) {
0547: refresh(file, REPOSITORY_STATUS_UNKNOWN);
0548: }
0549: }
0550: }
0551: }
0552:
0553: // --- Private methods ---------------------------------------------------
0554:
0555: private Map<File, FileInformation> getScannedFiles(File dir) {
0556: Map<File, FileInformation> files;
0557:
0558: // there are 2nd level nested admin dirs (.svn/tmp, .svn/prop-base, ...)
0559:
0560: if (svn.isAdministrative(dir)) {
0561: return NOT_MANAGED_MAP;
0562: }
0563: File parent = dir.getParentFile();
0564: if (parent != null && svn.isAdministrative(parent)) {
0565: return NOT_MANAGED_MAP;
0566: }
0567:
0568: files = (Map<File, FileInformation>) turbo.readEntry(dir,
0569: FILE_STATUS_MAP);
0570: if (files != null)
0571: return files;
0572: if (isNotManagedByDefault(dir)) {
0573: return NOT_MANAGED_MAP;
0574: }
0575:
0576: // scan and populate cache with results
0577:
0578: dir = FileUtil.normalizeFile(dir);
0579: files = scanFolder(dir); // must not execute while holding the lock, it may take long to execute
0580: assert files.containsKey(dir) == false;
0581: turbo.writeEntry(dir, FILE_STATUS_MAP, files);
0582: for (Iterator i = files.keySet().iterator(); i.hasNext();) {
0583: File file = (File) i.next();
0584: FileInformation info = files.get(file);
0585: if ((info.getStatus() & FileInformation.STATUS_LOCAL_CHANGE) != 0) {
0586: fireFileStatusChanged(file, null, info);
0587: }
0588: }
0589: return files;
0590: }
0591:
0592: private boolean isNotManagedByDefault(File dir) {
0593: return !dir.exists();
0594: }
0595:
0596: /**
0597: * Scans all files in the given folder, computes and stores their CVS status.
0598: *
0599: * @param dir directory to scan
0600: * @return Map map to be included in the status cache (File => FileInformation)
0601: */
0602: private Map<File, FileInformation> scanFolder(File dir) {
0603: File[] files = dir.listFiles();
0604: if (files == null)
0605: files = new File[0];
0606: Map<File, FileInformation> folderFiles = new HashMap<File, FileInformation>(
0607: files.length);
0608:
0609: ISVNStatus[] entries = null;
0610: try {
0611: if (Subversion.getInstance().isManaged(dir)) {
0612: SvnClient client = Subversion.getInstance().getClient(
0613: true);
0614: entries = client.getStatus(dir, false, false);
0615: }
0616: } catch (SVNClientException e) {
0617: // no or damaged entries
0618: //LOG.getDefault().annotate(e, "Can not status " + dir.getAbsolutePath() + ", guessing it..."); // NOI18N
0619: SvnClientExceptionHandler.notifyException(e, false, false);
0620: }
0621:
0622: if (entries == null) {
0623: for (int i = 0; i < files.length; i++) {
0624: File file = files[i];
0625: if (svn.isAdministrative(file))
0626: continue;
0627: FileInformation fi = createFileInformation(file, null,
0628: REPOSITORY_STATUS_UNKNOWN);
0629: if (fi.isDirectory()
0630: || fi.getStatus() != FileInformation.STATUS_VERSIONED_UPTODATE) {
0631: folderFiles.put(file, fi);
0632: }
0633: }
0634: } else {
0635: Set<File> localFiles = new HashSet<File>(Arrays
0636: .asList(files));
0637: for (int i = 0; i < entries.length; i++) {
0638: ISVNStatus entry = entries[i];
0639: File file = new File(entry.getPath());
0640: if (file.equals(dir)) {
0641: continue;
0642: }
0643: localFiles.remove(file);
0644: if (svn.isAdministrative(file)) {
0645: continue;
0646: }
0647: FileInformation fi = createFileInformation(file, entry,
0648: REPOSITORY_STATUS_UNKNOWN);
0649: if (fi.isDirectory()
0650: || fi.getStatus() != FileInformation.STATUS_VERSIONED_UPTODATE) {
0651: folderFiles.put(file, fi);
0652: }
0653: }
0654:
0655: Iterator it = localFiles.iterator();
0656: while (it.hasNext()) {
0657: File localFile = (File) it.next();
0658: FileInformation fi = createFileInformation(localFile,
0659: null, REPOSITORY_STATUS_UNKNOWN);
0660: if (fi.isDirectory()
0661: || fi.getStatus() != FileInformation.STATUS_VERSIONED_UPTODATE) {
0662: folderFiles.put(localFile, fi);
0663: }
0664: }
0665: }
0666:
0667: return folderFiles;
0668: }
0669:
0670: /**
0671: * Examines a file or folder and computes its status.
0672: *
0673: * @param status entry for this file or null if the file is unknown to subversion
0674: * @return FileInformation file/folder status bean
0675: */
0676: private FileInformation createFileInformation(File file,
0677: ISVNStatus status, ISVNStatus repositoryStatus) {
0678: if (status == null
0679: || status.getTextStatus().equals(
0680: SVNStatusKind.UNVERSIONED)) {
0681: if (!svn.isManaged(file)) {
0682: return file.isDirectory() ? FILE_INFORMATION_NOTMANAGED_DIRECTORY
0683: : FILE_INFORMATION_NOTMANAGED;
0684: }
0685: return createMissingEntryFileInformation(file,
0686: repositoryStatus);
0687: } else {
0688: return createVersionedFileInformation(file, status,
0689: repositoryStatus);
0690: }
0691: }
0692:
0693: /**
0694: * Examines a file or folder that has an associated CVS entry.
0695: *
0696: * @param file file/folder to examine
0697: * @param status status of the file/folder as reported by the CVS server
0698: * @return FileInformation file/folder status bean
0699: */
0700: private FileInformation createVersionedFileInformation(File file,
0701: ISVNStatus status, ISVNStatus repositoryStatus) {
0702:
0703: SVNStatusKind kind = status.getTextStatus();
0704: SVNStatusKind pkind = status.getPropStatus();
0705:
0706: int remoteStatus = 0;
0707: if (repositoryStatus != REPOSITORY_STATUS_UNKNOWN) {
0708: if (repositoryStatus.getRepositoryTextStatus() == SVNStatusKind.MODIFIED
0709: || repositoryStatus.getRepositoryPropStatus() == SVNStatusKind.MODIFIED) {
0710: remoteStatus = FileInformation.STATUS_VERSIONED_MODIFIEDINREPOSITORY;
0711: } else if (repositoryStatus.getRepositoryTextStatus() == SVNStatusKind.DELETED
0712: /*|| repositoryStatus.getRepositoryPropStatus() == SVNStatusKind.DELETED*/) {
0713: remoteStatus = FileInformation.STATUS_VERSIONED_REMOVEDINREPOSITORY;
0714: } else if (repositoryStatus.getRepositoryTextStatus() == SVNStatusKind.ADDED
0715: /*|| repositoryStatus.getRepositoryPropStatus() == SVNStatusKind.ADDED*/) {
0716: // solved in createMissingfileInformation
0717: } else if (repositoryStatus.getRepositoryTextStatus() == null
0718: && repositoryStatus.getRepositoryPropStatus() == null) {
0719: // no remote change at all
0720: } else {
0721: // TODO systematically handle all repository statuses
0722: // so far above were observed....
0723: // XXX
0724: System.err
0725: .println("SVN.FSC: unhandled repository status: "
0726: + file.getAbsolutePath()); // NOI18N
0727: System.err.println("\ttext: "
0728: + repositoryStatus.getRepositoryTextStatus()); // NOI18N
0729: System.err.println("\tprop: "
0730: + repositoryStatus.getRepositoryPropStatus()); // NOI18N
0731: }
0732: }
0733:
0734: if (status.getLockOwner() != null) {
0735: remoteStatus = FileInformation.STATUS_LOCKED | remoteStatus;
0736: }
0737:
0738: if (SVNStatusKind.NONE.equals(pkind)) {
0739: // no influence
0740: } else if (SVNStatusKind.NORMAL.equals(pkind)) {
0741: // no influence
0742: } else if (SVNStatusKind.MODIFIED.equals(pkind)) {
0743: if (SVNStatusKind.NORMAL.equals(kind)) {
0744: return new FileInformation(
0745: FileInformation.STATUS_VERSIONED_MODIFIEDLOCALLY
0746: | remoteStatus, status);
0747: }
0748: } else if (SVNStatusKind.CONFLICTED.equals(pkind)) {
0749: return new FileInformation(
0750: FileInformation.STATUS_VERSIONED_CONFLICT
0751: | remoteStatus, status);
0752: } else {
0753: throw new IllegalArgumentException("Unknown prop status: "
0754: + status.getPropStatus()); // NOI18N
0755: }
0756:
0757: if (SVNStatusKind.NONE.equals(kind)) {
0758: return FILE_INFORMATION_UNKNOWN;
0759: } else if (SVNStatusKind.NORMAL.equals(kind)) {
0760: return new FileInformation(
0761: FileInformation.STATUS_VERSIONED_UPTODATE
0762: | remoteStatus, status);
0763: } else if (SVNStatusKind.MODIFIED.equals(kind)) {
0764: return new FileInformation(
0765: FileInformation.STATUS_VERSIONED_MODIFIEDLOCALLY
0766: | remoteStatus, status);
0767: } else if (SVNStatusKind.ADDED.equals(kind)) {
0768: return new FileInformation(
0769: FileInformation.STATUS_VERSIONED_ADDEDLOCALLY
0770: | remoteStatus, status);
0771: } else if (SVNStatusKind.DELETED.equals(kind)) {
0772: return new FileInformation(
0773: FileInformation.STATUS_VERSIONED_REMOVEDLOCALLY
0774: | remoteStatus, status);
0775: } else if (SVNStatusKind.UNVERSIONED.equals(kind)) {
0776: return new FileInformation(
0777: FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY
0778: | remoteStatus, status);
0779: } else if (SVNStatusKind.MISSING.equals(kind)) {
0780: return new FileInformation(
0781: FileInformation.STATUS_VERSIONED_DELETEDLOCALLY
0782: | remoteStatus, status);
0783: } else if (SVNStatusKind.REPLACED.equals(kind)) {
0784: // XXX create new status constant? Is it neccesary to visualize
0785: // this status or better to use this simplyfication?
0786: return new FileInformation(
0787: FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY
0788: | remoteStatus, status);
0789: } else if (SVNStatusKind.MERGED.equals(kind)) {
0790: return new FileInformation(
0791: FileInformation.STATUS_VERSIONED_MERGE
0792: | remoteStatus, status);
0793: } else if (SVNStatusKind.CONFLICTED.equals(kind)) {
0794: return new FileInformation(
0795: FileInformation.STATUS_VERSIONED_CONFLICT
0796: | remoteStatus, status);
0797: } else if (SVNStatusKind.OBSTRUCTED.equals(kind)) {
0798: // TODO: create new status constant?
0799: return new FileInformation(
0800: FileInformation.STATUS_VERSIONED_CONFLICT
0801: | remoteStatus, status);
0802: } else if (SVNStatusKind.IGNORED.equals(kind)) {
0803: return new FileInformation(
0804: FileInformation.STATUS_NOTVERSIONED_EXCLUDED
0805: | remoteStatus, status);
0806: } else if (SVNStatusKind.INCOMPLETE.equals(kind)) {
0807: // TODO: create new status constant?
0808: return new FileInformation(
0809: FileInformation.STATUS_VERSIONED_CONFLICT
0810: | remoteStatus, status);
0811: } else if (SVNStatusKind.EXTERNAL.equals(kind)) {
0812: // TODO: create new status constant?
0813: return new FileInformation(
0814: FileInformation.STATUS_VERSIONED_UPTODATE
0815: | remoteStatus, status);
0816: } else {
0817: throw new IllegalArgumentException("Unknown text status: "
0818: + status.getTextStatus()); // NOI18N
0819: }
0820: }
0821:
0822: static String statusText(ISVNStatus status) {
0823: return "file: " + status.getTextStatus().toString()
0824: + " copied: " + status.isCopied() + " prop: "
0825: + status.getPropStatus().toString(); // NOI18N
0826: }
0827:
0828: /**
0829: * Examines a file or folder that does NOT have an associated Subversion status.
0830: *
0831: * @param file file/folder to examine
0832: * @return FileInformation file/folder status bean
0833: */
0834: private FileInformation createMissingEntryFileInformation(
0835: File file, ISVNStatus repositoryStatus) {
0836:
0837: // ignored status applies to whole subtrees
0838: boolean isDirectory = file.isDirectory();
0839: int parentStatus = getStatus(file.getParentFile()).getStatus();
0840: if (parentStatus == FileInformation.STATUS_NOTVERSIONED_EXCLUDED) {
0841: return isDirectory ? FILE_INFORMATION_EXCLUDED_DIRECTORY
0842: : FILE_INFORMATION_EXCLUDED;
0843: }
0844: if (parentStatus == FileInformation.STATUS_NOTVERSIONED_NOTMANAGED) {
0845: if (isDirectory) {
0846: // Working directory roots (aka managed roots). We already know that isManaged(file) is true
0847: return SvnUtils.isPartOfSubversionMetadata(file) ? FILE_INFORMATION_NOTMANAGED_DIRECTORY
0848: : FILE_INFORMATION_UPTODATE_DIRECTORY;
0849: } else {
0850: return FILE_INFORMATION_NOTMANAGED;
0851: }
0852: }
0853:
0854: // mark auxiliary after-update conflict files as ignored
0855: // C source.java
0856: // I source.java.mine
0857: // I source.java.r45
0858: // I source.java.r57
0859: //
0860: // XXX:svnClientAdapter design: why is not it returned from getSingleStatus() ?
0861: //
0862: // after-merge conflicts (even svn st does not recognize as ignored)
0863: // C source.java
0864: // ? source.java.working
0865: // ? source.jave.merge-right.r20
0866: // ? source.java.merge-left.r0
0867: //
0868: // XXX:svn-cli design: why is not it returned from getSingleStatus() ?
0869:
0870: String name = file.getName();
0871: Matcher m = auxConflictPattern.matcher(name);
0872: if (m.matches()) {
0873: File dir = file.getParentFile();
0874: if (dir != null) {
0875: String masterName = m.group(1);
0876: File master = new File(dir, masterName);
0877: if (master.isFile()) {
0878: return FILE_INFORMATION_EXCLUDED;
0879: }
0880: }
0881: }
0882:
0883: if (file.exists()) {
0884: if (Subversion.getInstance().isIgnored(file)) {
0885: return new FileInformation(
0886: FileInformation.STATUS_NOTVERSIONED_EXCLUDED,
0887: file.isDirectory());
0888: } else {
0889: return new FileInformation(
0890: FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY,
0891: file.isDirectory());
0892: }
0893: } else {
0894: if (repositoryStatus != REPOSITORY_STATUS_UNKNOWN) {
0895: if (repositoryStatus.getRepositoryTextStatus() == SVNStatusKind.ADDED) {
0896: boolean folder = repositoryStatus.getNodeKind() == SVNNodeKind.DIR;
0897: return new FileInformation(
0898: FileInformation.STATUS_VERSIONED_NEWINREPOSITORY,
0899: folder);
0900: }
0901: }
0902: return FILE_INFORMATION_UNKNOWN;
0903: }
0904: }
0905:
0906: private boolean exists(File file) {
0907: if (!file.exists())
0908: return false;
0909: return file.getAbsolutePath().equals(
0910: FileUtil.normalizeFile(file).getAbsolutePath());
0911: }
0912:
0913: ListenersSupport listenerSupport = new ListenersSupport(this );
0914:
0915: public void addVersioningListener(VersioningListener listener) {
0916: listenerSupport.addListener(listener);
0917: }
0918:
0919: public void removeVersioningListener(VersioningListener listener) {
0920: listenerSupport.removeListener(listener);
0921: }
0922:
0923: private void fireFileStatusChanged(File file,
0924: FileInformation oldInfo, FileInformation newInfo) {
0925: listenerSupport.fireVersioningEvent(EVENT_FILE_STATUS_CHANGED,
0926: new Object[] { file, oldInfo, newInfo });
0927: }
0928:
0929: public void setCommand(int command) {
0930: // boring ISVNNotifyListener event
0931: }
0932:
0933: public void logCommandLine(String commandLine) {
0934: // boring ISVNNotifyListener event
0935: }
0936:
0937: public void logMessage(String message) {
0938: // boring ISVNNotifyListener event
0939: }
0940:
0941: public void logError(String message) {
0942: // boring ISVNNotifyListener event
0943: }
0944:
0945: public void logRevision(long revision, String path) {
0946: // boring ISVNNotifyListener event
0947: }
0948:
0949: public void logCompleted(String message) {
0950: // boring ISVNNotifyListener event
0951: }
0952:
0953: public void onNotify(final File path, final SVNNodeKind kind) {
0954: // notifications from the svnclientadapter may be caused by a synchronously handled FS event.
0955: // The thing is that we have to prevent reentrant calls on the FS api ...
0956: Utils.post(new Runnable() {
0957: public void run() {
0958: onNotifyImpl(path, kind);
0959: }
0960: });
0961: }
0962:
0963: private void onNotifyImpl(File path, SVNNodeKind kind) {
0964: if (path == null) { // on kill
0965: return;
0966: }
0967:
0968: // I saw "./"
0969: path = FileUtil.normalizeFile(path);
0970:
0971: // ISVNNotifyListener event
0972: // invalidate cached status
0973: // force event: an updated file changes status from uptodate to uptodate but its entry changes
0974: refresh(path, REPOSITORY_STATUS_UNKNOWN, true);
0975:
0976: // collect the filesystems to notify them in SvnClientInvocationHandler about the external change
0977: for (;;) {
0978: FileObject fo = FileUtil.toFileObject(path);
0979: if (fo != null) {
0980: try {
0981: Set<FileSystem> filesystems = getFilesystemsToRefresh();
0982: synchronized (filesystems) {
0983: filesystems.add(fo.getFileSystem());
0984: }
0985: } catch (FileStateInvalidException e) {
0986: // ignore invalid filesystems
0987: }
0988: break;
0989: } else {
0990: path = path.getParentFile();
0991: if (path == null)
0992: break;
0993: }
0994: }
0995: }
0996:
0997: public void refreshDirtyFileSystems() {
0998: if (refreshFilesystemsTask == null) {
0999: RequestProcessor rp = new RequestProcessor();
1000: refreshFilesystemsTask = rp.create(new Runnable() {
1001: public void run() {
1002: Set<FileSystem> filesystems = getFilesystemsToRefresh();
1003: FileSystem[] filesystemsToRefresh = new FileSystem[filesystems
1004: .size()];
1005: synchronized (filesystems) {
1006: filesystemsToRefresh = filesystems
1007: .toArray(new FileSystem[filesystems
1008: .size()]);
1009: filesystems.clear();
1010: }
1011: for (int i = 0; i < filesystemsToRefresh.length; i++) {
1012: // don't call refresh() in synchronized (filesystems). It may lead to a deadlock.
1013: filesystemsToRefresh[i].refresh(true);
1014: }
1015: }
1016: });
1017: }
1018: refreshFilesystemsTask.schedule(200);
1019: }
1020:
1021: private Set<FileSystem> getFilesystemsToRefresh() {
1022: if (filesystemsToRefresh == null) {
1023: filesystemsToRefresh = new HashSet<FileSystem>();
1024: }
1025: return filesystemsToRefresh;
1026: }
1027:
1028: private static final class NotManagedMap extends
1029: AbstractMap<File, FileInformation> {
1030: public Set<Entry<File, FileInformation>> entrySet() {
1031: return Collections.emptySet();
1032: }
1033: }
1034: }
|