| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.netbeans.modules.subversion; |
| |
| import java.awt.EventQueue; |
| import java.util.Map.Entry; |
| import org.netbeans.modules.versioning.util.FileUtils; |
| import org.netbeans.modules.subversion.util.SvnUtils; |
| import org.netbeans.modules.subversion.client.SvnClient; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.*; |
| import java.util.logging.Level; |
| import org.netbeans.modules.subversion.client.SvnClientExceptionHandler; |
| import org.netbeans.modules.subversion.client.SvnClientFactory; |
| import org.netbeans.modules.subversion.notifications.NotificationsManager; |
| import org.netbeans.modules.subversion.ui.status.StatusAction; |
| import org.netbeans.modules.subversion.util.Context; |
| import org.netbeans.modules.subversion.util.SvnSearchHistorySupport; |
| import org.netbeans.modules.versioning.spi.VCSInterceptor; |
| import org.netbeans.modules.versioning.util.SearchHistorySupport; |
| import org.netbeans.modules.versioning.util.Utils; |
| import org.openide.util.Exceptions; |
| import org.openide.util.NbBundle; |
| import org.openide.util.RequestProcessor; |
| import org.openide.util.Utilities; |
| import org.tigris.subversion.svnclientadapter.*; |
| |
| /** |
| * Handles events fired from the filesystem such as file/folder create/delete/move. |
| * |
| * @author Maros Sandor |
| */ |
| class FilesystemHandler extends VCSInterceptor { |
| |
| private final FileStatusCache cache; |
| |
| /** |
| * Stores all moved files for a later cache refresh in afterMove |
| */ |
| private final Set<File> movedFiles = new HashSet<File>(); |
| private final Set<File> copiedFiles = new HashSet<File>(); |
| |
| private final Set<File> internalyDeletedFiles = new HashSet<File>(); |
| private final Set<File> toLockFiles = Collections.synchronizedSet(new HashSet<File>()); |
| private final Map<File, Boolean> readOnlyFiles = Collections.synchronizedMap(new LinkedHashMap<File, Boolean>() { |
| @Override |
| protected boolean removeEldestEntry (Entry<File, Boolean> eldest) { |
| return size() > 100; |
| } |
| }); |
| private static final RequestProcessor RP = new RequestProcessor("Subversion FileSystemHandler", 1, false, false); //NOI18N |
| |
| /** |
| * Stores .svn folders that should be deleted ASAP. |
| */ |
| private final Set<File> invalidMetadata = new HashSet<File>(5); |
| private static final int STATUS_VCS_MODIFIED_ATTRIBUTE |
| = FileInformation.STATUS_VERSIONED_CONFLICT |
| | FileInformation.STATUS_VERSIONED_MERGE |
| | FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY |
| | FileInformation.STATUS_VERSIONED_ADDEDLOCALLY |
| | FileInformation.STATUS_VERSIONED_MODIFIEDLOCALLY; |
| public FilesystemHandler(Subversion svn) { |
| cache = svn.getStatusCache(); |
| } |
| |
| @Override |
| public boolean beforeDelete(File file) { |
| Subversion.LOG.log(Level.FINE, "beforeDelete {0}", file); |
| if(!SvnClientFactory.isClientAvailable()) { |
| Subversion.LOG.fine(" skipping delete due to missing client"); |
| return false; |
| } |
| if (SvnUtils.isPartOfSubversionMetadata(file)) return true; |
| // calling cache results in SOE, we must check manually |
| return isVersioned(file.getParentFile()); |
| } |
| |
| /** |
| * This interceptor ensures that subversion metadata is NOT deleted. |
| * |
| * @param file file to delete |
| */ |
| @Override |
| public void doDelete(File file) throws IOException { |
| Subversion.LOG.log(Level.FINE, "doDelete {0}", file); |
| if (!SvnUtils.isPartOfSubversionMetadata(file)) { |
| try { |
| SvnClient client = Subversion.getInstance().getClient(false); |
| try { |
| client.remove(new File [] { file }, true); // delete all files recursively |
| return; |
| } catch (SVNClientException ex) { |
| // not interested, will continue and ask isVersioned() |
| } |
| /** |
| * Copy a folder, it becames svn added/copied. |
| * Revert its parent and check 'Delete new files', the folder becomes unversioned. |
| * 'Delete new files' deletes the folder and invokes this method. |
| * But client.remove cannot be called since the folder is unversioned and we do not want to propagate an exception |
| */ |
| if (isVersioned(file.getParentFile())) { |
| client.remove(new File [] { file }, true); // delete all files recursively |
| } |
| // with the cache refresh we rely on afterDelete |
| } catch (SVNClientException e) { |
| if (!WorkingCopyAttributesCache.getInstance().isSuppressed(e)) { |
| SvnClientExceptionHandler.notifyException(e, false, false); // log this |
| } |
| IOException ex = new IOException(); |
| Exceptions.attachLocalizedMessage(ex, NbBundle.getMessage(FilesystemHandler.class, "MSG_DeleteFailed", new Object[] {file, e.getLocalizedMessage()})); // NOI18N |
| ex.getCause().initCause(e); |
| throw ex; |
| } finally { |
| internalyDeletedFiles.add(file); |
| } |
| } |
| } |
| |
| @Override |
| public void afterDelete(final File file) { |
| Subversion.LOG.log(Level.FINE, "afterDelete {0}", file); |
| if (file == null || SvnUtils.isPartOfSubversionMetadata(file)) return; |
| |
| // TODO the afterXXX events should not be triggered by the FS listener events |
| // their order isn't guaranteed when e.g calling fo.delete() a fo.create() |
| // in an atomic action |
| |
| // check if delete already handled |
| if(internalyDeletedFiles.remove(file)) { |
| // file was already deleted we only have to refresh the cache |
| cache.refreshAsync(file); |
| return; |
| } |
| |
| // there was no doDelete event for the file -> |
| // could be deleted externaly, so try to handle it by 'svn rm' too |
| RP.post(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| File parent = file.getParentFile(); |
| if(parent != null && !parent.exists()) { |
| return; |
| } |
| try { |
| SvnClient client = Subversion.getInstance().getClient(false); |
| if (shallRemove(client, file)) { |
| client.remove(new File [] { file }, true); |
| } |
| } catch (SVNClientException e) { |
| // ignore; we do not know what to do here |
| Subversion.LOG.log(Level.FINER, null, e); |
| } |
| } finally { |
| cache.refreshAsync(file); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Moves folder's content between different repositories. |
| * Does not move folders, only files inside them. |
| * The created tree in the target working copy is created without subversion metadata. |
| * @param from folder being moved. MUST be a folder. |
| * @param to a folder from's content shall be moved into |
| * @throws java.io.IOException if error occurs |
| * @throws org.tigris.subversion.svnclientadapter.SVNClientException if error occurs |
| */ |
| private void moveFolderToDifferentRepository(File from, File to) throws IOException, SVNClientException { |
| assert from.isDirectory(); |
| assert to.getParentFile().exists(); |
| if (!to.exists()) { |
| if (to.mkdir()) { |
| cache.refreshAsync(to); |
| } else { |
| Subversion.LOG.log(Level.WARNING, "{0}: Cannot create folder {1}", new Object[]{FilesystemHandler.class.getName(), to}); |
| } |
| } |
| File[] files = from.listFiles(); |
| for (File file : files) { |
| if (!SvnUtils.isAdministrative(file)) { |
| svnMoveImplementation(file, new File(to, file.getName())); |
| } |
| } |
| } |
| |
| /** |
| * Copies folder's content between different repositories. |
| * Does not copy folders, only files inside them. |
| * The created tree in the target working copy is created without subversion metadata. |
| * @param from folder being copied. MUST be a folder. |
| * @param to a folder from's content shall be copied into |
| * @throws java.io.IOException if error occurs |
| * @throws org.tigris.subversion.svnclientadapter.SVNClientException if error occurs |
| */ |
| private void copyFolderToDifferentRepository(File from, File to) throws IOException, SVNClientException { |
| assert from.isDirectory(); |
| assert to.getParentFile().exists(); |
| if (!to.exists()) { |
| if (to.mkdir()) { |
| cache.refreshAsync(to); |
| } else { |
| Subversion.LOG.log(Level.WARNING, "{0}: Cannot create folder {1}", new Object[]{FilesystemHandler.class.getName(), to}); |
| } |
| } |
| File[] files = from.listFiles(); |
| for (File file : files) { |
| if (!SvnUtils.isAdministrative(file)) { |
| svnCopyImplementation(file, new File(to, file.getName())); |
| } |
| } |
| } |
| |
| /** |
| * Tries to determine if the <code>file</code> is supposed to be really removed by svn or not.<br/> |
| * i.e. unversioned files should not be removed at all. |
| * @param file file sheduled for removal |
| * @return <code>true</code> if the <code>file</code> shall be really removed, <code>false</code> otherwise. |
| */ |
| private boolean shallRemove(SvnClient client, File file) throws SVNClientException { |
| boolean retval = true; |
| if (!"true".equals(System.getProperty("org.netbeans.modules.subversion.deleteMissingFiles", "false"))) { //NOI18N |
| // prevents automatic svn remove for those who dislike such behavior |
| Subversion.LOG.log(Level.FINE, "File {0} deleted externally, metadata not repaired (org.netbeans.modules.subversion.deleteMissingFiles=false by default)", new String[] {file.getAbsolutePath()}); //NOI18N |
| retval = false; |
| } else { |
| ISVNStatus status = getStatus(client, file); |
| if (!SVNStatusKind.MISSING.equals(status.getTextStatus())) { |
| Subversion.LOG.fine(" shallRemove: skipping delete due to correct metadata"); |
| retval = false; |
| } else if (Utilities.isMac() || Utilities.isWindows()) { |
| String existingFilename = FileUtils.getExistingFilenameInParent(file); |
| if (existingFilename != null) { |
| retval = false; |
| } |
| } |
| } |
| return retval; |
| } |
| |
| @Override |
| public boolean beforeMove(File from, File to) { |
| Subversion.LOG.log(Level.FINE, "beforeMove {0} -> {1}", new Object[]{from, to}); |
| if(!SvnClientFactory.isClientAvailable()) { |
| Subversion.LOG.fine(" skipping move due to missing client"); |
| return false; |
| } |
| File destDir = to.getParentFile(); |
| if (from != null && destDir != null) { |
| // a direct cache call could, because of the synchrone beforeMove handling, |
| // trigger an reentrant call on FS => we have to check manually |
| if (isVersioned(from) || isVersioned(to)) { |
| return SvnUtils.isManaged(to); |
| } |
| // else XXX handle file with saved administative |
| // right now they have old status in cache but is it guaranteed? |
| } |
| return false; |
| } |
| |
| @Override |
| public void doMove(final File from, final File to) throws IOException { |
| Subversion.LOG.log(Level.FINE, "doMove {0} -> {1}", new Object[]{from, to}); |
| svnMoveImplementation(from, to); |
| } |
| |
| @Override |
| public void afterMove(final File from, final File to) { |
| Subversion.LOG.log(Level.FINE, "afterMove {0} -> {1}", new Object[]{from, to}); |
| File[] files; |
| synchronized(movedFiles) { |
| movedFiles.add(from); |
| files = movedFiles.toArray(new File[movedFiles.size()]); |
| movedFiles.clear(); |
| } |
| cache.refreshAsync(true, to); // refresh the whole target tree |
| cache.refreshAsync(files); |
| File parent = to.getParentFile(); |
| if (parent != null) { |
| if (from.equals(to)) { |
| Subversion.LOG.log(Level.WARNING, "Wrong (identity) rename event for {0}", from.getAbsolutePath()); |
| } |
| } |
| } |
| |
| @Override |
| public boolean beforeCopy(File from, File to) { |
| Subversion.LOG.log(Level.FINE, "beforeCopy {0} -> {1}", new Object[]{from, to}); |
| if(!SvnClientFactory.isClientAvailable()) { |
| Subversion.LOG.fine(" skipping copy due to missing client"); |
| return false; |
| } |
| |
| File destDir = to.getParentFile(); |
| if (from != null && destDir != null) { |
| // a direct cache call could, because of the synchrone beforeCopy handling, |
| // trigger an reentrant call on FS => we have to check manually |
| if (isVersioned(from) || isVersioned(to)) { |
| |
| if(from.isDirectory()) { |
| // always handle copy of versioned folders. |
| // we have to take care of metadata even if svn copy can't |
| // be called - e.g when copy into an unversioned folder filesystem would also |
| // try to copy the .svn folder and fail |
| return true; |
| } else { |
| return SvnUtils.isManaged(to); |
| } |
| } |
| // else XXX handle file with saved administative |
| // right now they have old status in cache but is it guaranteed? |
| } |
| |
| return false; |
| } |
| |
| @Override |
| public void doCopy(final File from, final File to) throws IOException { |
| Subversion.LOG.log(Level.FINE, "doCopy {0} -> {1}", new Object[]{from, to}); |
| svnCopyImplementation(from, to); |
| } |
| |
| @Override |
| public void afterCopy(final File from, final File to) { |
| Subversion.LOG.log(Level.FINE, "afterCopy {0} -> {1}", new Object[]{from, to}); |
| File[] files; |
| synchronized(copiedFiles) { |
| copiedFiles.add(from); |
| files = copiedFiles.toArray(new File[copiedFiles.size()]); |
| copiedFiles.clear(); |
| } |
| cache.refreshAsync(true, to); // refresh the whole target tree |
| cache.refreshAsync(files); |
| File parent = to.getParentFile(); |
| if (parent != null) { |
| if (from.equals(to)) { |
| Subversion.LOG.log(Level.WARNING, "Wrong (identity) rename event for {0}", from.getAbsolutePath()); |
| } |
| } |
| } |
| |
| private void svnCopyImplementation(final File from, final File to) throws IOException { |
| try { |
| SvnClient client = Subversion.getInstance().getClient(false); |
| |
| // prepare destination, it must be under Subversion control |
| removeInvalidMetadata(); |
| |
| File parent; |
| if (to.isDirectory()) { |
| parent = to; |
| } else { |
| parent = to.getParentFile(); |
| } |
| |
| boolean parentManaged = false; |
| boolean parentIgnored = false; |
| if (parent != null) { |
| parentManaged = SvnUtils.isManaged(parent); |
| // a direct cache call could, because of the synchrone svnMove/CopyImplementation handling, |
| // trigger an reentrant call on FS => we have to check manually |
| if (parentManaged && !isVersioned(parent)) { |
| parentIgnored = !addDirectories(parent); |
| } |
| } |
| |
| // perform |
| int retryCounter = 6; |
| while (true) { |
| try { |
| ISVNStatus toStatus = getStatus(client, to); |
| |
| // check the status - if the file isn't in the repository yet ( ADDED | UNVERSIONED ) |
| // then it also can't be moved via the svn client |
| ISVNStatus status = getStatus(client, from); |
| |
| // store all from-s children -> they also have to be refreshed in after copy |
| List<File> srcChildren = null; |
| try { |
| srcChildren = SvnUtils.listManagedRecursively(from); |
| if (parentIgnored) { |
| // do not svn copy into ignored folders |
| if(!copyFile(from, to)) { |
| Subversion.LOG.log(Level.INFO, "Cannot copy file {0} to {1}", new Object[] {from, to}); |
| } |
| } else if (status != null && (status.getTextStatus().equals(SVNStatusKind.UNVERSIONED) |
| || status.getTextStatus().equals(SVNStatusKind.IGNORED))) { // ignored file CAN'T be moved via svn |
| // check if the file wasn't just deleted in this session |
| revertDeleted(client, toStatus, to, true); |
| |
| if(!copyFile(from, to)) { |
| Subversion.LOG.log(Level.INFO, "Cannot copy file {0} to {1}", new Object[] {from, to}); |
| } |
| } else { |
| SVNUrl repositorySource = SvnUtils.getRepositoryRootUrl(from); |
| SVNUrl repositoryTarget = parentManaged ? SvnUtils.getRepositoryRootUrl(parent) : null; |
| if (parentManaged && repositorySource.equals(repositoryTarget)) { |
| // use client.copy only for a single repository |
| client.copy(from, to); |
| } else { |
| // copy into unversioned folder or |
| // from a repository into another |
| if (from.isDirectory()) { |
| // tree should be copied separately, |
| // otherwise the metadata from the source WC will be copied too |
| copyFolderToDifferentRepository(from, to); |
| } else if (copyFile(from, to)) { |
| Subversion.LOG.log(Level.FINE, FilesystemHandler.class.getName() |
| + ": copying between different repositories {0} to {1}", new Object[] {from, to}); |
| } else { |
| Subversion.LOG.log(Level.WARNING, FilesystemHandler.class.getName() |
| + ": cannot copy {0} to {1}", new Object[] {from, to}); |
| } |
| } |
| } |
| break; |
| } finally { |
| // we moved the files so schedule them a for a refresh |
| // in the following afterMove call |
| synchronized(copiedFiles) { |
| if(srcChildren != null) { |
| copiedFiles.addAll(srcChildren); |
| } |
| } |
| } |
| } catch (SVNClientException e) { |
| // svn: Working copy '/tmp/co/svn-prename-19/AnagramGame-pack-rename/src/com/toy/anagrams/ui2' locked |
| if (e.getMessage().endsWith("' locked") && retryCounter > 0) { // NOI18N |
| // XXX HACK AWT- or FS Monitor Thread performs |
| // concurrent operation |
| try { |
| Thread.sleep(107); |
| } catch (InterruptedException ex) { |
| // ignore |
| } |
| retryCounter--; |
| continue; |
| } |
| if (!WorkingCopyAttributesCache.getInstance().isSuppressed(e)) { |
| SvnClientExceptionHandler.notifyException(e, false, false); // log this |
| } |
| IOException ex = new IOException(); |
| Exceptions.attachLocalizedMessage(ex, NbBundle.getMessage(FilesystemHandler.class, "MSG_MoveFailed", new Object[] {from, to, e.getLocalizedMessage()})); // NOI18N |
| ex.getCause().initCause(e); |
| throw ex; |
| } |
| } |
| } catch (SVNClientException e) { |
| if (!WorkingCopyAttributesCache.getInstance().isSuppressed(e)) { |
| SvnClientExceptionHandler.notifyException(e, false, false); // log this |
| } |
| IOException ex = new IOException(); |
| Exceptions.attachLocalizedMessage(ex, "Subversion failed to move " + from.getAbsolutePath() + " to: " + to.getAbsolutePath() + "\n" + e.getLocalizedMessage()); // NOI18N |
| ex.getCause().initCause(e); |
| throw ex; |
| } |
| |
| } |
| |
| @Override |
| public boolean beforeCreate(File file, boolean isDirectory) { |
| Subversion.LOG.log(Level.FINE, "beforeCreate {0}", file); |
| if(!SvnClientFactory.isClientAvailable()) { |
| Subversion.LOG.fine(" skipping create due to missing client"); |
| return false; |
| } |
| if (SvnUtils.isPartOfSubversionMetadata(file)) { |
| synchronized(invalidMetadata) { |
| File p = file; |
| while(!SvnUtils.isAdministrative(p.getName())) { |
| p = p.getParentFile(); |
| assert p != null : "file " + file + " doesn't have a .svn parent"; |
| } |
| invalidMetadata.add(p); |
| } |
| return false; |
| } else { |
| if (!file.exists()) { |
| try { |
| SvnClient client = Subversion.getInstance().getClient(false); |
| // check if the file wasn't just deleted in this session |
| revertDeleted(client, file, true); |
| } catch (SVNClientException ex) { |
| if (!WorkingCopyAttributesCache.getInstance().isSuppressed(ex)) { |
| SvnClientExceptionHandler.notifyException(ex, false, false); |
| } |
| } |
| } |
| return false; |
| } |
| } |
| |
| @Override |
| public void doCreate(File file, boolean isDirectory) throws IOException { |
| // do nothing |
| } |
| |
| @Override |
| public void afterCreate(final File file) { |
| Subversion.LOG.log(Level.FINE, "afterCreate {0}", file); |
| if (SvnUtils.isPartOfSubversionMetadata(file)) { |
| // not interested in .svn events |
| return; |
| } |
| RP.post(new Runnable() { |
| @Override |
| public void run() { |
| if (file == null) return; |
| // I. refresh cache |
| int status = cache.refresh(file, FileStatusCache.REPOSITORY_STATUS_UNKNOWN).getStatus(); |
| if ((status & FileInformation.STATUS_MANAGED) == 0) { |
| return; |
| } |
| if (file.isDirectory()) { |
| // II. refresh the whole dir |
| cache.directoryContentChanged(file); |
| } else if ((status & FileInformation.STATUS_VERSIONED_REMOVEDLOCALLY) != 0 && file.exists()) { |
| // file exists but it's status is set to deleted |
| File temporary = FileUtils.generateTemporaryFile(file.getParentFile(), file.getName()); |
| try { |
| SvnClient client = Subversion.getInstance().getClient(false); |
| if (file.renameTo(temporary)) { |
| client.revert(file, false); |
| file.delete(); |
| } else { |
| Subversion.LOG.log(Level.WARNING, "FileSystemHandler.afterCreate: cannot rename {0} to {1}", new Object[] { file, temporary }); //NOI18N |
| client.addFile(file); // at least add the file so it is not deleted |
| } |
| } catch (SVNClientException ex) { |
| Subversion.LOG.log(Level.INFO, null, ex); |
| } finally { |
| if (temporary.exists()) { |
| try { |
| if (!temporary.renameTo(file)) { |
| Subversion.LOG.log(Level.WARNING, "FileSystemHandler.afterCreate: cannot rename {0} back to {1}, {1} exists={2}", new Object[] { temporary, file, file.exists() }); //NOI18N |
| FileUtils.copyFile(temporary, file); |
| } |
| } catch (IOException ex) { |
| Subversion.LOG.log(Level.INFO, "FileSystemHandler.afterCreate: cannot copy {0} back to {1}", new Object[] { temporary, file }); //NOI18N |
| } finally { |
| temporary.delete(); |
| } |
| } |
| cache.refresh(file, FileStatusCache.REPOSITORY_STATUS_UNKNOWN).getStatus(); |
| } |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void afterChange(final File file) { |
| if(!SvnClientFactory.isClientAvailable()) { |
| Subversion.LOG.fine(" skipping afterChange due to missing client"); |
| return; |
| } |
| Subversion.LOG.log(Level.FINE, "afterChange {0}", file); |
| RP.post(new Runnable() { |
| @Override |
| public void run() { |
| if ((cache.getStatus(file).getStatus() & FileInformation.STATUS_MANAGED) != 0) { |
| cache.refresh(file, FileStatusCache.REPOSITORY_STATUS_UNKNOWN); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public Object getAttribute(final File file, String attrName) { |
| if("ProvidedExtensions.RemoteLocation".equals(attrName)) { |
| return getRemoteRepository(file); |
| } else if("ProvidedExtensions.Refresh".equals(attrName)) { |
| return new Runnable() { |
| @Override |
| public void run() { |
| if (!SvnClientFactory.isClientAvailable()) { |
| Subversion.LOG.fine(" skipping ProvidedExtensions.Refresh due to missing client"); //NOI18N |
| return; |
| } |
| if (!SvnUtils.isManaged(file)) { |
| return; |
| } |
| try { |
| SvnClient client = Subversion.getInstance().getClient(file); |
| if (client != null) { |
| Subversion.getInstance().getStatusCache().refreshCached(new Context(file)); |
| StatusAction.executeStatus(file, client, null, false); // no need to contact server |
| } |
| } catch (SVNClientException ex) { |
| SvnClientExceptionHandler.notifyException(ex, true, true); |
| return; |
| } |
| } |
| }; |
| } else if (SearchHistorySupport.PROVIDED_EXTENSIONS_SEARCH_HISTORY.equals(attrName)){ |
| return new SvnSearchHistorySupport(file); |
| } else if ("ProvidedExtensions.VCSIsModified".equals(attrName)) { |
| |
| if (file == null) { |
| return null; |
| } |
| |
| if (!SvnClientFactory.isClientAvailable()) { |
| Subversion.LOG.fine(" skipping ProvidedExtensions.VCSIsModified due to missing client"); //NOI18N |
| return null; |
| } |
| if (!SvnUtils.isManaged(file)) { |
| return null; |
| } |
| try { |
| SvnClient client = Subversion.getInstance().getClient(file); |
| if (client != null) { |
| Context ctx = new Context(file); |
| Subversion.getInstance().getStatusCache().refreshCached(ctx); |
| StatusAction.executeStatus(file, client, null, false); // no need to contact server |
| return cache.containsFiles(ctx, STATUS_VCS_MODIFIED_ATTRIBUTE, true); |
| } |
| } catch (SVNClientException ex) { |
| SvnClientExceptionHandler.notifyException(ex, false, false); |
| } |
| return null; |
| } else { |
| return super.getAttribute(file, attrName); |
| } |
| } |
| |
| @Override |
| public void beforeEdit (final File file) { |
| if (cache.ready()) { |
| NotificationsManager.getInstance().scheduleFor(file); |
| } |
| ensureLocked(file); |
| } |
| |
| @Override |
| public long refreshRecursively(File dir, long lastTimeStamp, List<? super File> children) { |
| long retval = -1; |
| if (SvnUtils.isAdministrative(dir.getName())) { |
| retval = 0; |
| } |
| return retval; |
| } |
| |
| @Override |
| public boolean isMutable(File file) { |
| boolean mutable = SvnUtils.isPartOfSubversionMetadata(file) || super.isMutable(file); |
| if (!mutable && SvnModuleConfig.getDefault().isAutoLock() && !readOnlyFiles.containsKey(file)) { |
| toLockFiles.add(file); |
| return true; |
| } |
| return mutable; |
| } |
| |
| private String getRemoteRepository(File file) { |
| if(file == null) return null; |
| SVNUrl url = null; |
| try { |
| url = SvnUtils.getRepositoryRootUrl(file); |
| } catch (SVNClientException ex) { |
| Subversion.LOG.log(Level.FINE, "No repository root url found for managed file : [" + file + "]", ex); //NOI18N |
| try { |
| url = SvnUtils.getRepositoryUrl(file); // try to falback |
| } catch (SVNClientException ex1) { |
| Subversion.LOG.log(Level.FINE, "No repository url found for managed file : [" + file + "]", ex1); |
| } |
| } |
| return url != null ? SvnUtils.decodeToString(url) : null; |
| } |
| |
| /** |
| * Removes invalid metadata from all known folders. |
| */ |
| void removeInvalidMetadata() { |
| synchronized(invalidMetadata) { |
| for (File file : invalidMetadata) { |
| Utils.deleteRecursively(file); |
| } |
| invalidMetadata.clear(); |
| } |
| } |
| |
| // private methods --------------------------- |
| |
| private boolean hasMetadata(File file) { |
| return new File(file, SvnUtils.SVN_ENTRIES_DIR).canRead(); |
| } |
| |
| private boolean isVersioned(File file) { |
| if (SvnUtils.isPartOfSubversionMetadata(file)) return false; |
| if (( !file.isFile() && hasMetadata(file) ) || ( file.isFile() && hasMetadata(file.getParentFile()) )) { |
| return true; |
| } |
| try { |
| SVNStatusKind statusKind = SvnUtils.getSingleStatus(Subversion.getInstance().getClient(false), file).getTextStatus(); |
| return statusKind != SVNStatusKind.UNVERSIONED && statusKind != SVNStatusKind.IGNORED; |
| } catch (SVNClientException ex) { |
| return false; |
| } |
| } |
| |
| /** |
| * Returns all direct parent folders from the given file which are scheduled for deletion |
| * |
| * @param file |
| * @param client |
| * @return a list of folders |
| * @throws org.tigris.subversion.svnclientadapter.SVNClientException |
| */ |
| private static List<File> getDeletedParents(File file, SvnClient client) throws SVNClientException { |
| List<File> ret = new ArrayList<File>(); |
| for(File parent = file.getParentFile(); parent != null; parent = parent.getParentFile()) { |
| ISVNStatus status = getStatus(client, parent); |
| if (status == null || !status.getTextStatus().equals(SVNStatusKind.DELETED)) { |
| return ret; |
| } |
| ret.add(parent); |
| } |
| return ret; |
| } |
| |
| private void revertDeleted(SvnClient client, final File file, boolean checkParents) { |
| try { |
| ISVNStatus status = getStatus(client, file); |
| revertDeleted(client, status, file, checkParents); |
| } catch (SVNClientException ex) { |
| if (!WorkingCopyAttributesCache.getInstance().isSuppressed(ex)) { |
| SvnClientExceptionHandler.notifyException(ex, false, false); |
| } |
| } |
| } |
| |
| private void revertDeleted(SvnClient client, ISVNStatus status, final File file, boolean checkParents) { |
| try { |
| if (FilesystemHandler.this.equals(status, SVNStatusKind.DELETED)) { |
| if(checkParents) { |
| // we have a file scheduled for deletion but it's going to be created again, |
| // => it's parent folder can't stay deleted either |
| final List<File> deletedParents = getDeletedParents(file, client); |
| // XXX JAVAHL client.revert(deletedParents.toArray(new File[deletedParents.size()]), false); |
| for (File parent : deletedParents) { |
| client.revert(parent, false); |
| } |
| if (!deletedParents.isEmpty()) { |
| Subversion.getInstance().getStatusCache().refreshAsync(deletedParents.toArray(new File[deletedParents.size()])); |
| } |
| } |
| |
| // reverting the file will set the metadata uptodate |
| client.revert(file, false); |
| // our goal was ony to fix the metadata -> |
| // -> get rid of the reverted file |
| internalyDeletedFiles.add(file); // prevents later removal in afterDelete if the file is recreated |
| file.delete(); |
| } |
| } catch (SVNClientException ex) { |
| if (!WorkingCopyAttributesCache.getInstance().isSuppressed(ex)) { |
| SvnClientExceptionHandler.notifyException(ex, false, false); |
| } |
| } |
| } |
| |
| private void svnMoveImplementation(final File from, final File to) throws IOException { |
| try { |
| boolean force = true; // file with local changes must be forced |
| SvnClient client = Subversion.getInstance().getClient(false); |
| |
| // prepare destination, it must be under Subversion control |
| removeInvalidMetadata(); |
| |
| File parent; |
| if (to.isDirectory()) { |
| parent = to; |
| } else { |
| parent = to.getParentFile(); |
| } |
| |
| boolean parentIgnored = false; |
| if (parent != null) { |
| assert SvnUtils.isManaged(parent) : "Cannot move " + from.getAbsolutePath() + " to " + to.getAbsolutePath() + ", " + parent.getAbsolutePath() + " is not managed"; // NOI18N see implsMove above |
| // a direct cache call could, because of the synchrone svnMoveImplementation handling, |
| // trigger an reentrant call on FS => we have to check manually |
| if (!isVersioned(parent)) { |
| parentIgnored = !addDirectories(parent); |
| } |
| } |
| |
| // perform |
| int retryCounter = 6; |
| while (true) { |
| try { |
| ISVNStatus toStatus = getStatus(client, to); |
| |
| // check the status - if the file isn't in the repository yet ( ADDED | UNVERSIONED ) |
| // then it also can't be moved via the svn client |
| ISVNStatus status = getStatus(client, from); |
| |
| // store all from-s children -> they also have to be refreshed in after move |
| List<File> srcChildren = null; |
| SVNUrl url = status != null && status.isCopied() ? getCopiedUrl(client, from) : null; |
| SVNUrl toUrl = toStatus != null ? toStatus.getUrl() : null; |
| try { |
| srcChildren = SvnUtils.listManagedRecursively(from); |
| boolean moved = true; |
| if (status != null |
| && (status.getTextStatus().equals(SVNStatusKind.ADDED) || status.getTextStatus().equals(SVNStatusKind.REPLACED)) |
| && (!status.isCopied() || (url != null && url.equals(toUrl)))) { |
| // 1. file is ADDED (new or added) AND is not COPIED (by invoking svn copy) |
| // 2. file is ADDED and COPIED (by invoking svn copy) and target equals the original from the first copy |
| // otherwise svn move should be invoked |
| |
| File temp = from; |
| if (Utilities.isWindows() && from.equals(to) || Utilities.isMac() && from.getPath().equalsIgnoreCase(to.getPath())) { |
| Subversion.LOG.log(Level.FINE, "svnMoveImplementation: magic workaround for filename case change {0} -> {1}", new Object[] { from, to }); //NOI18N |
| temp = FileUtils.generateTemporaryFile(from.getParentFile(), from.getName()); |
| Subversion.LOG.log(Level.FINE, "svnMoveImplementation: magic workaround, step 1: {0} -> {1}", new Object[] { from, temp }); //NOI18N |
| client.move(from, temp, force); |
| } |
| |
| // check if the file wasn't just deleted in this session |
| revertDeleted(client, toStatus, to, true); |
| |
| moved = temp.renameTo(to); |
| if (moved) { |
| // indeed just ADDED, not REPLACED |
| if (status.getTextStatus().equals(SVNStatusKind.ADDED)) { |
| client.revert(temp, true); |
| } else { |
| client.remove(new File[] { temp }, true); |
| } |
| } |
| } else if (status != null && (status.getTextStatus().equals(SVNStatusKind.UNVERSIONED) |
| || status.getTextStatus().equals(SVNStatusKind.IGNORED))) { // ignored file CAN'T be moved via svn |
| // check if the file wasn't just deleted in this session |
| revertDeleted(client, toStatus, to, true); |
| |
| moved = from.renameTo(to); |
| } else if (parentIgnored) { |
| // parent is ignored so do not add the file |
| moved = from.renameTo(to); |
| client.remove(new File[] { from }, true); |
| } else { |
| SVNUrl repositorySource = SvnUtils.getRepositoryRootUrl(from); |
| SVNUrl repositoryTarget = SvnUtils.getRepositoryRootUrl(parent); |
| if (repositorySource.equals(repositoryTarget)) { |
| // use client.move only for a single repository |
| try { |
| client.move(from, to, force); |
| } catch (SVNClientException ex) { |
| if (Utilities.isWindows() && from.equals(to) || Utilities.isMac() && from.getPath().equalsIgnoreCase(to.getPath())) { |
| Subversion.LOG.log(Level.FINE, "svnMoveImplementation: magic workaround for filename case change {0} -> {1}", new Object[] { from, to }); //NOI18N |
| File temp = FileUtils.generateTemporaryFile(to.getParentFile(), from.getName()); |
| Subversion.LOG.log(Level.FINE, "svnMoveImplementation: magic workaround, step 1: {0} -> {1}", new Object[] { from, temp }); //NOI18N |
| client.move(from, temp, force); |
| Subversion.LOG.log(Level.FINE, "svnMoveImplementation: magic workaround, step 2: {0} -> {1}", new Object[] { temp, to }); //NOI18N |
| client.move(temp, to, force); |
| Subversion.LOG.log(Level.FINE, "svnMoveImplementation: magic workaround completed"); //NOI18N |
| } else { |
| throw ex; |
| } |
| } |
| } else { |
| boolean remove = false; |
| if (from.isDirectory()) { |
| // tree should be moved separately, otherwise the metadata from the source WC will be copied too |
| moveFolderToDifferentRepository(from, to); |
| remove = true; |
| } else if (from.renameTo(to)) { |
| remove = true; |
| } else { |
| Subversion.LOG.log(Level.WARNING, FilesystemHandler.class.getName() |
| + ": cannot rename {0} to {1}", new Object[] {from, to}); |
| } |
| if (remove) { |
| client.remove(new File[] {from}, force); |
| Subversion.LOG.log(Level.FINE, FilesystemHandler.class.getName() |
| + ": moving between different repositories {0} to {1}", new Object[] {from, to}); |
| } |
| } |
| } |
| if (!moved) { |
| Subversion.LOG.log(Level.INFO, "Cannot rename file {0} to {1}", new Object[] {from, to}); |
| } |
| } finally { |
| // we moved the files so schedule them a for a refresh |
| // in the following afterMove call |
| synchronized(movedFiles) { |
| if(srcChildren != null) { |
| movedFiles.addAll(srcChildren); |
| } |
| } |
| } |
| break; |
| } catch (SVNClientException e) { |
| // svn: Working copy '/tmp/co/svn-prename-19/AnagramGame-pack-rename/src/com/toy/anagrams/ui2' locked |
| if (e.getMessage().endsWith("' locked") && retryCounter > 0) { // NOI18N |
| // XXX HACK AWT- or FS Monitor Thread performs |
| // concurrent operation |
| try { |
| Thread.sleep(107); |
| } catch (InterruptedException ex) { |
| // ignore |
| } |
| retryCounter--; |
| continue; |
| } |
| if (!WorkingCopyAttributesCache.getInstance().isSuppressed(e)) { |
| SvnClientExceptionHandler.notifyException(e, false, false); // log this |
| } |
| IOException ex = new IOException(); |
| Exceptions.attachLocalizedMessage(ex, NbBundle.getMessage(FilesystemHandler.class, "MSG_MoveFailed", new Object[] {from, to, e.getLocalizedMessage()})); //NOI18N |
| ex.getCause().initCause(e); |
| throw ex; |
| } |
| } |
| } catch (SVNClientException e) { |
| if (!WorkingCopyAttributesCache.getInstance().isSuppressed(e)) { |
| SvnClientExceptionHandler.notifyException(e, false, false); // log this |
| } |
| IOException ex = new IOException(); |
| Exceptions.attachLocalizedMessage(ex, "Subversion failed to move " + from.getAbsolutePath() + " to: " + to.getAbsolutePath() + "\n" + e.getLocalizedMessage()); // NOI18N |
| ex.getCause().initCause(e); |
| throw ex; |
| } |
| } |
| |
| /** |
| * Seeks versioned root and then adds all folders |
| * under Subversion (so it contains metadata), |
| */ |
| private boolean addDirectories(final File dir) throws SVNClientException { |
| SvnClient client = Subversion.getInstance().getClient(false); |
| ISVNStatus s = getStatus(client, dir); |
| if(s.getTextStatus().equals(SVNStatusKind.IGNORED)) { |
| return false; |
| } |
| File parent = dir.getParentFile(); |
| if (parent != null) { |
| if (SvnUtils.isManaged(parent) && !isVersioned(parent)) { |
| if(!addDirectories(parent)) { // RECURSION |
| return false; |
| } |
| } |
| client.addDirectory(dir, false); |
| cache.refreshAsync(dir); |
| return true; |
| } else { |
| throw new SVNClientException("Reached FS root, but it's still not Subversion versioned!"); // NOI18N |
| } |
| } |
| |
| private static ISVNStatus getStatus(SvnClient client, File file) throws SVNClientException { |
| // a direct cache call could, because of the synchrone beforeCreate handling, |
| // trigger an reentrant call on FS => we have to check manually |
| return SvnUtils.getSingleStatus(client, file); |
| } |
| |
| private boolean equals(ISVNStatus status, SVNStatusKind kind) { |
| return status != null && status.getTextStatus().equals(kind); |
| } |
| |
| private boolean copyFile(File from, File to) { |
| try { |
| FileUtils.copyFile(from, to); |
| } catch (IOException ex) { |
| SvnClientExceptionHandler.notifyException(ex, false, false); // log this |
| return false; |
| } |
| return true; |
| } |
| |
| private void ensureLocked (final File file) { |
| if (toLockFiles.contains(file)) { |
| Runnable outsideAWT = new Runnable () { |
| @Override |
| public void run () { |
| boolean readOnly = true; |
| try { |
| // unlock files that... |
| // ... have svn:needs-lock prop set |
| SvnClient client = Subversion.getInstance().getClient(false); |
| boolean hasPropSet = false; |
| for (ISVNProperty prop : client.getProperties(file)) { |
| if ("svn:needs-lock".equals(prop.getName())) { //NOI18N |
| hasPropSet = true; |
| break; |
| } |
| } |
| if (hasPropSet) { |
| ISVNStatus status = SvnUtils.getSingleStatus(client, file); |
| // ... are not just added - lock does not make sense since the file is not in repo yet |
| if (status != null && status.getTextStatus() != SVNStatusKind.ADDED) { |
| SVNUrl url = SvnUtils.getRepositoryRootUrl(file); |
| if (url != null) { |
| client = Subversion.getInstance().getClient(url); |
| if (status.getLockOwner() != null) { |
| // the file is locked yet it's still read-only, it may be a result of: |
| // 1. svn lock A |
| // 2. svn move A B - B is new and read-only |
| // 3. move B A - A is now also read-only |
| client.unlock(new File[] { file }, false); //NOI18N |
| } |
| client.lock(new File[] { file }, "", false); //NOI18N |
| readOnly = false; |
| } |
| } |
| } |
| } catch (SVNClientException ex) { |
| SvnClientExceptionHandler.notifyException(ex, false, false); |
| readOnly = true; |
| } |
| if (readOnly) { |
| // conditions to unlock failed, set the file to read-only |
| readOnlyFiles.put(file, Boolean.TRUE); |
| } |
| toLockFiles.remove(file); |
| } |
| }; |
| if (EventQueue.isDispatchThread()) { |
| Subversion.getInstance().getRequestProcessor().post(outsideAWT); |
| } else { |
| outsideAWT.run(); |
| } |
| } |
| } |
| |
| private SVNUrl getCopiedUrl (SvnClient client, File f) { |
| try { |
| ISVNInfo info = SvnUtils.getInfoFromWorkingCopy(client, f); |
| if (info != null) { |
| return info.getCopyUrl(); |
| } |
| } catch (SVNClientException e) { |
| // at least log the exception |
| if (!WorkingCopyAttributesCache.getInstance().isSuppressed(e)) { |
| Subversion.LOG.log(Level.INFO, null, e); |
| } |
| } |
| return null; |
| } |
| |
| } |