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

Java tutorial

Introduction

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

Source

/*******************************************************************************
 * Copyright (c) 2006-2008 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:
 *     VecTrace (Zingo Andersen) - implementation
 *     Software Balm Consulting Inc (Peter Hunnisett <peter_hge at softwarebalm dot com>) - some updates
 *     StefanC                   - some updates
 *     Charles O'Farrell         - fix revert open file
 *     Andrei Loskutov           - bug fixes
 *******************************************************************************/
package com.vectrace.MercurialEclipse.team;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;

import com.aragost.javahg.Changeset;
import com.vectrace.MercurialEclipse.MercurialEclipsePlugin;
import com.vectrace.MercurialEclipse.SafeWorkspaceJob;
import com.vectrace.MercurialEclipse.commands.HgParentClient;
import com.vectrace.MercurialEclipse.commands.HgRevertClient;
import com.vectrace.MercurialEclipse.dialogs.RevertDialog;
import com.vectrace.MercurialEclipse.exception.HgException;
import com.vectrace.MercurialEclipse.menu.UpdateHandler;
import com.vectrace.MercurialEclipse.model.ChangeSet;
import com.vectrace.MercurialEclipse.model.HgRoot;
import com.vectrace.MercurialEclipse.model.JHgChangeSet;
import com.vectrace.MercurialEclipse.team.cache.LocalChangesetCache;
import com.vectrace.MercurialEclipse.team.cache.MercurialRootCache;
import com.vectrace.MercurialEclipse.team.cache.MercurialStatusCache;
import com.vectrace.MercurialEclipse.utils.ResourceUtils;

public class ActionRevert implements IWorkbenchWindowActionDelegate {
    private IWorkbenchWindow window;
    private IStructuredSelection selection;
    private JHgChangeSet changesetToRevert;

    public ActionRevert() {
        super();
    }

    /**
     * We can use this method to dispose of any system resources we previously
     * allocated.
     *
     * @see IWorkbenchWindowActionDelegate#dispose
     */
    public void dispose() {
    }

    /**
     * We will cache window object in order to be able to provide parent shell
     * for the message dialog.
     *
     * @see IWorkbenchWindowActionDelegate#init
     */
    public void init(IWorkbenchWindow w) {
        this.window = w;
    }

    /**
     * The action has been activated. The argument of the method represents the
     * 'real' action sitting in the workbench UI.
     *
     * @see IWorkbenchWindowActionDelegate#run
     */
    public void run(IAction action) {
        if (window == null) {
            window = MercurialEclipsePlugin.getActiveWindow();
        }
        IResource singleSelection = getSingleSelection();
        if (singleSelection != null) {
            if (changesetToRevert != null) {
                try {
                    revertToGivenVersion(singleSelection, changesetToRevert, new NullProgressMonitor());
                } catch (final HgException e) {
                    handleWithDialog(e);
                }
                return;
            }
            if (action != null
                    && "com.vectrace.MercurialEclipse.team.ReplaceWithParentAction".equals(action.getId())) {
                revertToParentVersion(singleSelection);
                return;
            }
        }

        List<IResource> resources = new ArrayList<IResource>();
        boolean mergeIsRunning = collectResourcesToRevert(resources);
        if (resources.size() > 0 && !mergeIsRunning) {
            openRevertDialog(resources, false);
        } else {
            if (mergeIsRunning) {
                boolean doRevert = MessageDialog.openConfirm(getShell(),
                        Messages.getString("ActionRevert.HgRevert"), //$NON-NLS-1$
                        Messages.getString("ActionRevert.mergeIsRunning")); //$NON-NLS-1$
                if (doRevert) {
                    openRevertDialog(resources, true);
                }
            } else {
                MessageDialog.openInformation(getShell(), Messages.getString("ActionRevert.HgRevert"), //$NON-NLS-1$
                        Messages.getString("ActionRevert.noFilesToRevert")); //$NON-NLS-1$
            }
        }
    }

    private static void handleWithDialog(final HgException e) {
        MercurialEclipsePlugin.logError(e);
        // TODO use statushandler???
        if (Display.getCurrent() != null) {
            MercurialEclipsePlugin.showError(e);
        } else {
            MercurialEclipsePlugin.getStandardDisplay().asyncExec(new Runnable() {
                public void run() {
                    MercurialEclipsePlugin.showError(e);
                }
            });
        }
    }

    private IResource getSingleSelection() {
        if (selection == null || selection.size() != 1) {
            return null;
        }
        return ResourceUtils.getResource(selection.getFirstElement());
    }

    private static JHgChangeSet getParentChangeset(IResource resource) throws HgException {
        HgRoot hgRoot = MercurialRootCache.getInstance().getHgRoot(resource);
        Changeset[] parents = HgParentClient.getParents(hgRoot, resource);
        ChangeSet cs = LocalChangesetCache.getInstance().get(hgRoot, parents[0]);

        if (cs != null && cs.getIndex() != 0) {
            return LocalChangesetCache.getInstance().get(hgRoot, cs.getParents()[0]);
        }
        return null;
    }

    private static void revertToParentVersion(final IResource resource) {
        Job job = new Job("Reverting to parent revision: " + ResourceUtils.getPath(resource)) {
            @Override
            protected IStatus run(IProgressMonitor monitor) {
                JHgChangeSet cs;
                try {
                    cs = getParentChangeset(resource);
                    revertToGivenVersion(resource, cs, monitor);
                } catch (HgException e) {
                    handleWithDialog(e);
                    return e.getStatus();
                }
                return Status.OK_STATUS;
            }
        };
        job.schedule();
    }

    private static void revertToGivenVersion(IResource resource, JHgChangeSet cs, IProgressMonitor monitor)
            throws HgException {
        HgRoot hgRoot = MercurialTeamProvider.getHgRoot(resource);
        if (hgRoot == null) {
            throw new HgException("Hg root not found for: " + resource);
        }
        List<IResource> list = new ArrayList<IResource>();
        list.add(resource);
        Collection<IResource> reverted = HgRevertClient.performRevert(monitor, hgRoot, list, cs);
        for (IResource res : reverted) {
            refreshResource(monitor, MercurialStatusCache.getInstance(), res);
        }
    }

    private Shell getShell() {
        return window != null ? window.getShell() : MercurialEclipsePlugin.getActiveShell();
    }

    private void openRevertDialog(List<IResource> resources, final boolean cleanAfterMerge) {
        final List<IResource> result;
        final List<IResource> untracked;
        RevertDialog chooser = null;
        if (!cleanAfterMerge) {
            chooser = new RevertDialog(Display.getCurrent().getActiveShell(), resources);
            if (chooser.open() != Window.OK) {
                return;
            }
            result = chooser.getSelectionForHgRevert();
            untracked = chooser.getUntrackedSelection();
        } else {
            result = resources;
            // TODO allow to revert untracked files after the merge too
            untracked = new ArrayList<IResource>();
        }

        final JHgChangeSet cs = chooser != null ? chooser.getChangeset() : null;

        new SafeWorkspaceJob(Messages.getString("ActionRevert.revertFiles")) { //$NON-NLS-1$
            @Override
            protected IStatus runSafe(IProgressMonitor monitor) {
                try {
                    doRevert(monitor, result, untracked, cleanAfterMerge, cs);
                } catch (HgException e) {
                    MercurialEclipsePlugin.logError(e);
                    return e.getStatus();
                }
                return Status.OK_STATUS;
            }
        }.schedule();
    }

    private boolean collectResourcesToRevert(List<IResource> resources) {
        boolean mergeIsRunning = false;
        for (Object obj : selection.toList()) {
            if (obj instanceof IResource) {
                IResource resource = (IResource) obj;
                boolean merging = MercurialStatusCache.getInstance().isMergeInProgress(resource);
                if (merging) {
                    mergeIsRunning = true;
                }
                boolean supervised = MercurialTeamProvider.isHgTeamProviderFor(resource);
                if (supervised) {
                    resources.add(resource);
                }
            }
        }
        return mergeIsRunning;
    }

    public void doRevert(IProgressMonitor monitor, List<IResource> resources, List<IResource> untracked,
            boolean cleanAfterMerge, JHgChangeSet cs) throws HgException {

        monitor.beginTask(Messages.getString("ActionRevert.revertingResources"), resources.size() * 2); //$NON-NLS-1$

        Map<IProject, List<IResource>> filesToRevert = ResourceUtils.groupByProject(resources);
        Set<IProject> projects = filesToRevert.keySet();

        // cleanup untracked files first
        deleteUntrackedFiles(untracked, monitor);

        // collect removed file state NOW
        MercurialStatusCache cache = MercurialStatusCache.getInstance();

        Map<IProject, Set<IResource>> removedFilesBefore = getFiles(MercurialStatusCache.BIT_REMOVED, projects);
        Map<IProject, Set<IResource>> addedFilesBefore = getFiles(MercurialStatusCache.BIT_ADDED,
                removedFilesBefore.keySet());

        // keep track only for projects where both add AND remove is reported
        removedFilesBefore.keySet().retainAll(addedFilesBefore.keySet());

        // perform revert
        Map<HgRoot, List<IResource>> rootToFiles = ResourceUtils.groupByRoot(resources);
        if (cleanAfterMerge) {
            for (Entry<HgRoot, List<IResource>> entry : rootToFiles.entrySet()) {
                performRevertAfterMerge(monitor, entry.getKey(), entry.getValue());
            }
        } else {
            for (Entry<HgRoot, List<IResource>> entry : rootToFiles.entrySet()) {
                HgRevertClient.performRevert(monitor, entry.getKey(), entry.getValue(), cs);
            }
        }

        for (IResource resource : resources) {
            monitor.subTask(Messages.getString("ActionRevert.refreshing") + resource.getName() + "..."); //$NON-NLS-1$ //$NON-NLS-2$
            refreshResource(monitor, cache, resource);
            monitor.worked(1);
        }

        for (Entry<IProject, Set<IResource>> entry : removedFilesBefore.entrySet()) {
            cache.refreshStatus(entry.getKey(), monitor);
        }

        // we are looking for files, which are NOT in the "reverted" files list
        // and which are NOT marked as deleted in the latest cached state
        // BUT which was marked as deleted before the revert operation.

        // These "deleted before, do not reverted and missing in the status now" files are
        // the source files recreated after the revert of a "move (rm + add)" operation.
        // We have to tell Eclipse, that the files are "living" again in the resources tree

        Map<IProject, Set<IResource>> filesToUpdate = new HashMap<IProject, Set<IResource>>();
        Map<IProject, Set<IResource>> removedFilesAfter = getFiles(MercurialStatusCache.BIT_REMOVED,
                removedFilesBefore.keySet());

        for (Entry<IProject, Set<IResource>> entry : removedFilesBefore.entrySet()) {
            IProject projBefore = entry.getKey();
            Set<IResource> removedBefore = entry.getValue();
            Set<IResource> removedAfter = removedFilesAfter.get(projBefore);
            if (removedAfter != null && !removedAfter.isEmpty()) {
                removedBefore.removeAll(removedAfter);
            }
            List<IResource> reverted = filesToRevert.get(projBefore);
            if (reverted != null && !reverted.isEmpty()) {
                removedBefore.removeAll(reverted);
            }
            if (!removedBefore.isEmpty()) {
                filesToUpdate.put(projBefore, removedBefore);
            }
        }

        for (Entry<IProject, Set<IResource>> entry : filesToUpdate.entrySet()) {
            Set<IResource> removed = entry.getValue();
            for (IResource resource : removed) {
                refreshResource(monitor, cache, resource);
            }
        }
        monitor.done();
    }

    /**
     * @param cache non null
     * @param resource non null
     */
    private static void refreshResource(IProgressMonitor monitor, MercurialStatusCache cache, IResource resource) {
        try {
            if (cache.isAdded(ResourceUtils.getPath(resource))) {
                // added files didn't change content after we un-add them, so we have
                // give Eclipse a hint to start some extra refresh work.
                ResourceUtils.touch(resource);
            }
            // we still need to trigger a refresh to avoid confusing editors opened on
            // these files. Without refresh, they complain that the files are changed but not refreshed
            resource.refreshLocal(IResource.DEPTH_ONE, monitor);
        } catch (CoreException e) {
            MercurialEclipsePlugin.logError(e);
        }
    }

    /**
     *  Deletes given resources and cleans up the cache state for them
     */
    private void deleteUntrackedFiles(List<IResource> untracked, IProgressMonitor monitor) {
        MercurialStatusCache cache = MercurialStatusCache.getInstance();
        for (IResource resource : untracked) {
            try {
                IContainer parent = null;
                if (resource instanceof IFile) {
                    parent = resource.getParent();
                    if (parent instanceof IProject && ".project".equals(resource.getName())) {
                        MercurialEclipsePlugin
                                .logInfo("Will NOT delete .project file from project " + parent.getName(), null);
                        // do not revert .project file....
                        continue;
                    }
                    resource.delete(IResource.FORCE | IResource.KEEP_HISTORY, monitor);
                } else if (!(resource instanceof IProject)) {
                    resource.delete(IResource.KEEP_HISTORY, monitor);
                }
                deleteEmptyDirs(parent, monitor);
                cache.clearStatusCache(resource);
            } catch (CoreException e) {
                MercurialEclipsePlugin.logError(e);
            }
        }
    }

    /**
     *  Recursive deletes empty directories, starting with given one
     */
    private void deleteEmptyDirs(IContainer dir, IProgressMonitor monitor) throws CoreException {
        int memberFlags = IContainer.INCLUDE_HIDDEN | IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS;
        if (dir != null && !(dir instanceof IProject) && dir.members(memberFlags).length == 0) {
            IContainer parent = dir.getParent();
            dir.delete(false, monitor);
            deleteEmptyDirs(parent, monitor);
        }
    }

    /**
     * @param projects
     * @param statusBit one of {@link MercurialStatusCache} status bits
     * @return a map where the files with the specified state are grouped by the project.
     * Projects with no files of given state are not included into the map
     */
    private static Map<IProject, Set<IResource>> getFiles(int statusBit, Set<IProject> projects) {
        MercurialStatusCache cache = MercurialStatusCache.getInstance();
        Map<IProject, Set<IResource>> resources = new HashMap<IProject, Set<IResource>>();
        for (IProject project : projects) {
            Set<IResource> removed = cache.getResources(statusBit, project);
            if (!removed.isEmpty()) {
                resources.put(project, removed);
            }
        }
        return resources;
    }

    private void performRevertAfterMerge(IProgressMonitor monitor, HgRoot hgRoot, List<IResource> resources) {
        // see http://mercurial.selenic.com/wiki/FAQ#FAQ.2BAC8-CommonProblems.hg_status_shows_changed_files_but_hg_diff_doesn.27t.21
        // To completely undo the uncommitted merge and discard all local modifications,
        // you will need to issue a hg update -C -r . (note the "dot" at the end of the command).
        try {
            UpdateHandler update = new UpdateHandler(false);
            update.setCleanEnabled(true);
            update.setRevision(".");
            update.setShell(getShell());
            update.run(hgRoot);
        } catch (Exception e) {
            MercurialEclipsePlugin.logError(e);
        }

    }

    /**
     * Selection in the workbench has been changed. We can change the state of
     * the 'real' action here if we want, but this can only happen after the
     * delegate has been created.
     *
     * @see IWorkbenchWindowActionDelegate#selectionChanged
     */
    public void selectionChanged(IAction action, ISelection inSelection) {
        if (inSelection != null && inSelection instanceof IStructuredSelection) {
            selection = (IStructuredSelection) inSelection;
        }
    }

    public void setChangesetToRevert(JHgChangeSet changesetToRevert) {
        this.changesetToRevert = changesetToRevert;
    }

}