com.vectrace.MercurialEclipse.team.HgMoveDeleteHook.java Source code

Java tutorial

Introduction

Here is the source code for com.vectrace.MercurialEclipse.team.HgMoveDeleteHook.java

Source

/*******************************************************************************
 * Copyright (c) 2006-2009 VecTrace (Zingo Andersen) and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Software Balm Consulting Inc (Peter Hunnisett <peter_hge at softwarebalm dot com>) - implementation
 *     VecTrace (Zingo Andersen) - some updates
 *     Stefan Groschupf          - logError
 *     Stefan C                  - Code cleanup
 *     Bastian Doetsch           - Code reformatting to code style and refreshes
 *     Andrei Loskutov           - bug fixes
 *******************************************************************************/
package com.vectrace.MercurialEclipse.team;

import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.internal.resources.ICoreConstants;
import org.eclipse.core.internal.resources.Resource;
import org.eclipse.core.internal.resources.ResourceInfo;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.team.IMoveDeleteHook;
import org.eclipse.core.resources.team.IResourceTree;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.widgets.Display;
import org.eclipse.team.core.RepositoryProvider;
import org.eclipse.team.core.TeamException;

import com.vectrace.MercurialEclipse.MercurialEclipsePlugin;
import com.vectrace.MercurialEclipse.commands.HgRemoveClient;
import com.vectrace.MercurialEclipse.commands.HgRenameClient;
import com.vectrace.MercurialEclipse.commands.HgRevertClient;
import com.vectrace.MercurialEclipse.dialogs.BaseCommitDialog.Options;
import com.vectrace.MercurialEclipse.exception.HgException;
import com.vectrace.MercurialEclipse.menu.CommitHandler;
import com.vectrace.MercurialEclipse.model.HgRoot;
import com.vectrace.MercurialEclipse.team.cache.MercurialRootCache;
import com.vectrace.MercurialEclipse.team.cache.MercurialStatusCache;
import com.vectrace.MercurialEclipse.team.cache.RefreshStatusJob;
import com.vectrace.MercurialEclipse.utils.ResourceUtils;

/**
 * @author Peter Hunnisett <peter_hge at softwarebalm dot com>
 *
 *         Hook into Eclipse rename and delete file operations so that the
 *         appropriate changes can be tracked in Mercurial as well.
 */
public class HgMoveDeleteHook implements IMoveDeleteHook {

    private static final MercurialStatusCache CACHE = MercurialStatusCache.getInstance();

    /**
     * @returns <code>true</code> if this file under this under Mercurial
     *          control.
     */
    private static boolean isInMercurialRepo(IResource file, IProgressMonitor monitor) {
        return CACHE.isSupervised(file);
    }

    /**
     * Determines if a folder has supervised files
     *
     * @returns <code>true</code> if there are files under this folder that are
     *          under Mercurial control.
     */
    private static boolean folderHasMercurialFiles(IFolder folder, IProgressMonitor monitor) {
        if (!isInMercurialRepo(folder, monitor)) {
            // Resource could be inside a link or something do nothing
            // in the future this could check is this is another repository
            return false;
        }

        try {
            IResource[] children = folder.members();
            for (IResource resource : children) {
                if (resource.getType() == IResource.FILE) {
                    if (resource.exists() && isInMercurialRepo(resource, monitor)) {
                        return true;
                    }
                } else {
                    if (folderHasMercurialFiles((IFolder) resource, monitor)) {
                        return true;
                    }
                }
            }
        } catch (CoreException e) {
            /*
             * Let's assume that this means there are no resources under this
             * one as it probably doesn't properly exist. Let eclipse do
             * everything.
             */
            return false;
        }

        return false;
    }

    /**
     * @see org.eclipse.core.resources.team.IMoveDeleteHook#deleteFile(org.eclipse.core.resources.team.IResourceTree, org.eclipse.core.resources.IFile, int, org.eclipse.core.runtime.IProgressMonitor)
     */
    public boolean deleteFile(IResourceTree tree, IFile file, int updateFlags, IProgressMonitor monitor) {
        /*
         * Returning false indicates that the caller should invoke
         * tree.standardDeleteFile to actually remove the resource from the file
         * system and eclipse.
         */

        if (!isInMercurialRepo(file, monitor) || file.isDerived() || file.isLinked()) {
            return false;
        }

        boolean keepHistory = (updateFlags & IResource.KEEP_HISTORY) != 0;
        if (keepHistory) {
            tree.addToLocalHistory(file);
        }
        return deleteHgFiles(tree, file, monitor);
    }

    /**
     * @see org.eclipse.core.resources.team.IMoveDeleteHook#deleteFolder(org.eclipse.core.resources.team.IResourceTree, org.eclipse.core.resources.IFolder, int, org.eclipse.core.runtime.IProgressMonitor)
     */
    public boolean deleteFolder(IResourceTree tree, IFolder folder, int updateFlags, IProgressMonitor monitor) {
        /*
         * Mercurial doesn't control directories. However, as a short cut
         * performing an operation on a folder will affect all subtending files.
         * Check that there is at least 1 file and if so there is Mercurial work
         * to do, otherwise there is no Mercurial work to be done.
         */
        if (!folderHasMercurialFiles(folder, monitor) || folder.isLinked() || folder.isDerived()) {
            return false;
        }

        /*
         * NOTE: There are bugs with Mercurial 0.9.1 on Windows and folder
         * delete/rename operation. See:
         * http://www.selenic.com/mercurial/bts/issue343,
         * http://www.selenic.com/mercurial/bts/issue303, etc. Returning false
         * indicates that the caller should invoke tree.standardDeleteFile to
         * actually remove the resource from the file system and eclipse.
         */
        return deleteHgFiles(tree, folder, monitor);
    }

    /**
     * Perform the file or folder (ie multiple file) delete.
     *
     * @returns <code>false</code> if the action succeeds, <code>true</code>
     *          otherwise. This syntax is to match the desired return code for
     *          <code>deleteFile</code> and <code>deleteFolder</code>.
     */
    private static boolean deleteHgFiles(IResourceTree tree, IResource resource, IProgressMonitor monitor) {
        // TODO: Decide if we should have different Hg behaviour based on the
        // force flag provided in updateFlags.
        try {
            // Delete the file(s) from the Mercurial repository.
            if (!resource.isLinked()) {
                if (!HgRemoveClient.forceRemoveResource(resource, monitor)) {
                    return false;
                }
            }
        } finally {
            try {
                MercurialStatusCache.getInstance().refreshStatus(resource, monitor);
            } catch (HgException e) {
                MercurialEclipsePlugin.logError(e);
                return false;
            }
        }

        // We removed the file ourselves, need to tell.
        if (resource.getType() == IResource.FOLDER) {
            tree.deletedFolder((IFolder) resource);
        } else if (resource.getType() == IResource.PROJECT) {
            tree.deletedProject((IProject) resource);
        } else {
            // hg deletes the parent folder too if the deleted file was the only one in the folder
            // we have to tell Eclipse that the folder (and probably all subsequent parents)
            // are deleted too...
            File dir = ResourceUtils.getFileHandle(resource).getParentFile();
            IContainer parent = resource.getParent();
            tree.deletedFile((IFile) resource);
            while (parent instanceof IFolder && dir != null && !dir.exists()) {
                IContainer backup = parent.getParent();
                tree.deletedFolder((IFolder) parent);
                parent = backup;
                dir = dir.getParentFile();
            }
        }

        // Returning true indicates that this method has removed resource in both
        // the file system and eclipse.
        return true;
    }

    public boolean deleteProject(IResourceTree tree, final IProject project, int updateFlags,
            IProgressMonitor monitor) {

        if ((updateFlags & IResource.ALWAYS_DELETE_PROJECT_CONTENT) == 0) {
            disconnect(project);
            tree.deletedProject(project);
            return true;
        }

        boolean isPartOfRepo = isContainedInRepository(project);
        if (isPartOfRepo) {
            final Set<IResource> allFiles = ResourceUtils.getMembers(project, false);
            allFiles.remove(project);
            try {
                HgRemoveClient.removeResources(new ArrayList<IResource>(allFiles));
            } finally {
                try {
                    MercurialStatusCache.getInstance().refreshStatus(project, monitor);
                } catch (HgException e) {
                    MercurialEclipsePlugin.logError(e);
                    MercurialEclipsePlugin.showError(e);
                }
            }

            final boolean[] continueDelete = new boolean[] { false };
            Display.getDefault().syncExec(new Runnable() {
                public void run() {
                    MessageDialog.openInformation(MercurialEclipsePlugin.getActiveShell(), "Project removed",
                            "All project files are now removed from Mercurial repository.\n"
                                    + "A commit is highly recommended if you wish to keep using this repository.");
                    CommitHandler ch = new CommitHandler();
                    Options options = new Options();
                    options.defaultCommitMessage = "Removed project '" + project.getName() + "' from repository.";
                    options.filesSelectable = false;
                    options.showAmend = false;
                    options.showCloseBranch = false;
                    options.showDiff = false;
                    options.showRevert = false;
                    ch.setOptions(options);
                    try {
                        ch.run(new ArrayList<IResource>(allFiles));
                    } catch (HgException e) {
                        MercurialEclipsePlugin.logError(e);
                    }
                    continueDelete[0] = ch.getResult() == Window.OK;
                }
            });
            if (continueDelete[0]) {
                disconnect(project);
                // if user committed deleted files, mercurial part is done
                // now we must say Eclipse please delete the project
                tree.deletedProject(project);
            }
            // delete was NOT done by hg. Anyway, let the files and project there.
            return true;
        }

        IFolder folder = project.getFolder(".hg"); //$NON-NLS-1$
        try {
            folder.delete(updateFlags, monitor);
            disconnect(project);
            // say Eclipse it should do the delete of now unmanaged project files for us
            return false;
        } catch (CoreException e) {
            MercurialEclipsePlugin.logError(e);
            return true;
        }
    }

    /**
     * @param project non null
     * @return true only if the project is located inside a (bigger) repository,
     * false if the project location equals repository location or unknown
     */
    private static boolean isContainedInRepository(final IProject project) {
        HgRoot hgRoot = MercurialTeamProvider.getHgRoot(project);
        if (hgRoot == null) {
            return false;
        }
        return !hgRoot.getIPath().equals(project.getLocation());
    }

    /**
     * @param project non null
     */
    private static void disconnect(final IProject project) {
        if (RepositoryProvider.isShared(project)) {
            try {
                RepositoryProvider.unmap(project);
            } catch (TeamException e) {
                MercurialEclipsePlugin.logError(e);
            }
        }
    }

    /**
     * @see org.eclipse.core.resources.team.IMoveDeleteHook#moveFile(org.eclipse.core.resources.team.IResourceTree, org.eclipse.core.resources.IFile, org.eclipse.core.resources.IFile, int, org.eclipse.core.runtime.IProgressMonitor)
     */
    public boolean moveFile(IResourceTree tree, IFile source, IFile destination, int updateFlags,
            IProgressMonitor monitor) {

        if (!isInMercurialRepo(source, monitor) || !isSameRoot(source, destination)) {
            return false;
        }
        boolean keepHistory = (updateFlags & IResource.KEEP_HISTORY) != 0;
        if (keepHistory) {
            tree.addToLocalHistory(source);
        }

        // Move the file in the Mercurial repository.
        if (!moveHgFiles(source, destination, monitor)) {
            return true;
        }

        // We moved the file ourselves, need to tell.
        tree.movedFile(source, destination);
        tree.updateMovedFileTimestamp(destination, tree.computeTimestamp(destination));

        // Returning true indicates that this method has moved resource in both
        // the file system and eclipse.
        return true;
    }

    /**
     * @see org.eclipse.core.resources.team.IMoveDeleteHook#moveFolder(org.eclipse.core.resources.team.IResourceTree, org.eclipse.core.resources.IFolder, org.eclipse.core.resources.IFolder, int, org.eclipse.core.runtime.IProgressMonitor)
     */
    public boolean moveFolder(IResourceTree tree, IFolder source, IFolder destination, int updateFlags,
            IProgressMonitor monitor) {
        /*
         * Mercurial doesn't control directories. However, as a short cut
         * performing an operation on a folder will affect all subtending files.
         * Check that there is at least 1 file and if so there is Mercurial work
         * to do, otherwise there is no Mercurial work to be done.
         */
        if (!folderHasMercurialFiles(source, monitor) || source.isLinked() || !isSameRoot(source, destination)) {
            return false;
        }

        // Move the folder (ie all subtending files) in the Mercurial
        // repository.
        if (!moveHgFiles(source, destination, monitor)) {
            //the move could have been partially successful, so refresh to ensure we are in sync
            try {
                source.refreshLocal(IResource.DEPTH_INFINITE, null);
                destination.refreshLocal(IResource.DEPTH_INFINITE, null);
            } catch (CoreException e) {
                //ignore secondary failures -we have already logged main failure
            }
            return true;
        }

        // We moved the file ourselves, need to tell.
        tree.movedFolderSubtree(source, destination);
        boolean isDeep = (updateFlags & IResource.SHALLOW) == 0;
        updateTimestamps(destination, isDeep, tree);

        // Returning true indicates that this method has moved resource in both
        // the file system and eclipse.
        return true;
    }

    private static boolean isSameRoot(IResource source, IResource dest) {
        MercurialRootCache cache = MercurialRootCache.getInstance();
        HgRoot root = cache.getHgRoot(source);

        return root != null && root.equals(cache.hasHgRoot(dest, true));
    }

    /**
     * Move the file or folder (ie multiple file).
     *
     * @returns <code>true</code> if the action succeeds, <code>false</code>
     *          otherwise.
     */
    private static boolean moveHgFiles(IResource source, IResource destination, IProgressMonitor monitor) {
        // Rename the file in the Mercurial repository.

        // TODO: Decide if we should have different Hg behavior based on the
        // force flag provided in updateFlags.
        try {
            HgRoot hgRoot = MercurialRootCache.getInstance().getHgRoot(source);
            IPath from = ResourceUtils.getPath(source);
            IPath to = ResourceUtils.getPath(destination);
            HgRenameClient.renameResource(from, to, hgRoot, monitor);
        } catch (final HgException e) {
            MercurialEclipsePlugin.logError(e);
            if (MercurialUtilities.isWindows() && source.getName().equalsIgnoreCase(destination.getName())) {
                try {
                    HgRoot hgRoot = MercurialTeamProvider.getHgRoot(source);
                    List<IResource> res = new ArrayList<IResource>();
                    res.add(source);
                    HgRevertClient.performRevert(monitor, hgRoot, res, null);
                } catch (HgException e1) {
                    MercurialEclipsePlugin.logError(e1);
                }
                HgException ex = new HgException("Mercurial does not support renaming of files on Windows,"
                        + " if file names differs only by the lower/upper case letters!", e);
                MercurialEclipsePlugin.showError(ex);
            }
            return false;
        }
        return true;
    }

    /**
     * @see org.eclipse.core.resources.team.IMoveDeleteHook#moveProject(org.eclipse.core.resources.team.IResourceTree, org.eclipse.core.resources.IProject, org.eclipse.core.resources.IProjectDescription, int, org.eclipse.core.runtime.IProgressMonitor)
     */
    public boolean moveProject(IResourceTree tree, final IProject source, IProjectDescription description,
            int flags, IProgressMonitor monitor) {

        // two cases: 1) project containing the repo or 2) project contained in the repo
        // for 1) we need to clean the cache
        // for the 2) we need to move if the project is relocated (not just renamed)
        boolean isPartOfRepo = isContainedInRepository(source);
        if (!isPartOfRepo) {
            // Eclipse will simply move *entire* repository by itself and change project name in .project file
            MercurialStatusCache.getInstance().clear(source, false);
            MercurialRootCache.getInstance().projectDeletedOrClosed(source);
            return false;
        }

        // project is located inside a (bigger) repository
        boolean locationChanged = isLocationChanged(source, description);
        if (!locationChanged) {
            // Don't care - only .project file will be changed due the name change
            return false;
        }

        // move all project related files inside a bigger hg repository
        final HgRoot hgRoot = MercurialRootCache.getInstance().getHgRoot(source);
        IPath from = source.getLocation();
        try {
            IPath to = computeDestination(description);
            HgRenameClient.renameResource(from, to, hgRoot, monitor);

            final boolean[] commitDone = new boolean[] { false };
            Display.getDefault().syncExec(new Runnable() {
                public void run() {
                    boolean commitNow = MessageDialog.openConfirm(MercurialEclipsePlugin.getActiveShell(),
                            "Project moved",
                            "All project files are now moved inside the Mercurial repository.\n"
                                    + "A commit NOW is highly recommended!\n"
                                    + "(otherwise you would need command line to commit removed files).\n\n"
                                    + "Commit ALL changed files NOW?");
                    if (commitNow) {
                        CommitHandler ch = new CommitHandler();
                        Options options = new Options();
                        options.defaultCommitMessage = "Moved project '" + source.getName() + "'.";
                        options.filesSelectable = false;
                        options.showAmend = false;
                        options.showCloseBranch = false;
                        options.showDiff = false;
                        options.showRevert = false;
                        options.allowEmptyCommit = true;
                        options.hgRoot = hgRoot;
                        ch.setOptions(options);
                        try {
                            ch.run(new ArrayList<IResource>());
                        } catch (HgException e) {
                            MercurialEclipsePlugin.logError(e);
                        }
                        commitDone[0] = ch.getResult() == Window.OK;
                    }
                }
            });

            if (tree.movedProjectSubtree(source, description)) {
                if (!commitDone[0]) {
                    Display.getDefault().syncExec(new Runnable() {
                        public void run() {
                            MessageDialog.openInformation(MercurialEclipsePlugin.getActiveShell(), "Project moved",
                                    "Source files are marked for delete but still not committed.\n"
                                            + "As the Eclipse project doesn't exist anymore at the original location, "
                                            + "please consider to commit from the command line.");
                        }
                    });
                }
                boolean isDeep = (flags & IResource.SHALLOW) == 0;
                IProject destination = source.getWorkspace().getRoot().getProject(description.getName());
                updateTimestamps(destination, isDeep, tree);
                return true;
            }
        } catch (Exception e) {
            MercurialEclipsePlugin.logError(e);
        } finally {
            new RefreshStatusJob("Updating cache", hgRoot).schedule(300);
        }
        return false;
    }

    private static IPath computeDestination(IProjectDescription description) throws CoreException {
        URI destLocation = description.getLocationURI();
        // Use the default area if necessary for the destination.
        if (destLocation == null) {
            IPath rootLocation = ResourcesPlugin.getWorkspace().getRoot().getLocation();
            destLocation = rootLocation.append(description.getName()).toFile().toURI();
        }
        IFileStore store = EFS.getStore(destLocation);
        File localFile = store.toLocalFile(0, null);
        return new Path(localFile.getAbsolutePath());
    }

    private static boolean isLocationChanged(IProject source, IProjectDescription description) {
        boolean locationChanged = false;
        try {
            URI srcURI = source.getDescription().getLocationURI();
            URI targetURI = description.getLocationURI();
            if (srcURI != null) {
                // easy: location was outside the workspace, so just compare with future location
                locationChanged = !srcURI.equals(targetURI);
            } else {
                if (targetURI != null) {
                    // project was inside workspace now outside
                    locationChanged = true;
                } else {
                    // both uri's are null: means that project was and will be located at default
                    // location (workspace), so the real OS location is changed
                    // if the project name is changed
                    locationChanged = !source.getName().equals(description.getName());
                }
            }
        } catch (CoreException e) {
            MercurialEclipsePlugin.logError(e);
        }
        return locationChanged;
    }

    /**
     * Helper method to update all the timestamps in the tree to match
     * those in the file system. Used after a #move.
     */
    private static void updateTimestamps(IResource root, final boolean isDeep, final IResourceTree tree) {
        IResourceVisitor visitor = new IResourceVisitor() {
            public boolean visit(IResource resource) {
                if (resource.isLinked()) {
                    if (isDeep) {
                        //clear the linked resource bit, if any
                        ResourceInfo info = ((Resource) resource).getResourceInfo(false, true);
                        info.clear(ICoreConstants.M_LINK);
                    }
                    return true;
                }
                //only needed if underlying file system does not preserve timestamps
                if (resource.getType() == IResource.FILE) {
                    IFile file = (IFile) resource;
                    tree.updateMovedFileTimestamp(file, tree.computeTimestamp(file));
                }
                return true;
            }
        };
        try {
            root.accept(visitor, IResource.DEPTH_INFINITE,
                    IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS | IContainer.INCLUDE_HIDDEN);
        } catch (CoreException e) {
            MercurialEclipsePlugin.logError(e);
        }
    }
}