com.vectrace.MercurialEclipse.history.MercurialHistory.java Source code

Java tutorial

Introduction

Here is the source code for com.vectrace.MercurialEclipse.history.MercurialHistory.java

Source

/*******************************************************************************
 * Copyright (c) 2007-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
 *     Stefan Groschupf          - logError
 *     Stefan C                  - Code cleanup
 *     Andrei Loskutov           - bugfixes
 *     Amenel Voglozin           - bug #485 (Show history across renames)
 *******************************************************************************/
package com.vectrace.MercurialEclipse.history;

import static com.vectrace.MercurialEclipse.preferences.MercurialPreferenceConstants.*;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.team.core.history.provider.FileHistory;

import com.aragost.javahg.Changeset;
import com.aragost.javahg.commands.ExecutionException;
import com.aragost.javahg.internals.GenericCommand;
import com.vectrace.MercurialEclipse.MercurialEclipsePlugin;
import com.vectrace.MercurialEclipse.commands.HgBisectClient;
import com.vectrace.MercurialEclipse.commands.HgBisectClient.Status;
import com.vectrace.MercurialEclipse.commands.HgClients;
import com.vectrace.MercurialEclipse.commands.HgLogClient;
import com.vectrace.MercurialEclipse.commands.HgParentClient;
import com.vectrace.MercurialEclipse.commands.HgStatusClient;
import com.vectrace.MercurialEclipse.commands.HgTagClient;
import com.vectrace.MercurialEclipse.commands.JavaHgCommandJob;
import com.vectrace.MercurialEclipse.commands.extensions.HgSigsClient;
import com.vectrace.MercurialEclipse.exception.HgException;
import com.vectrace.MercurialEclipse.history.GraphLayout.ParentProvider;
import com.vectrace.MercurialEclipse.model.ChangeSet;
import com.vectrace.MercurialEclipse.model.HgRoot;
import com.vectrace.MercurialEclipse.model.JHgChangeSet;
import com.vectrace.MercurialEclipse.model.Signature;
import com.vectrace.MercurialEclipse.model.Tag;
import com.vectrace.MercurialEclipse.preferences.MercurialPreferenceConstants;
import com.vectrace.MercurialEclipse.team.MercurialTeamProvider;
import com.vectrace.MercurialEclipse.team.MercurialUtilities;
import com.vectrace.MercurialEclipse.utils.BranchUtils;
import com.vectrace.MercurialEclipse.utils.ResourceUtils;

/**
 * @author zingo
 */
public class MercurialHistory extends FileHistory {

    private static final Comparator<ChangeSet> CS_COMPARATOR = new Comparator<ChangeSet>() {
        public int compare(ChangeSet o1, ChangeSet o2) {
            return o2.getIndex() - o1.getIndex();
        }
    };

    private final IResource resource;
    private final HgRoot hgRoot;
    private final List<MercurialRevision> revisions = new ArrayList<MercurialRevision>();
    private Tag[] tags;
    private int lastReqRevision;
    private boolean showTags;
    private boolean showGraph = true;
    private boolean bisectStarted;
    private GraphLayout layout;

    // constructors

    /**
     * @param resource must be non null
     */
    public MercurialHistory(IResource resource) {
        super();
        Assert.isNotNull(resource);
        HgRoot root = MercurialTeamProvider.getHgRoot(resource);
        if (root != null && root.getIPath().equals(ResourceUtils.getPath(resource))) {
            this.resource = null;
        } else {
            if (resource instanceof IFile && root != null) {
                IPath path = root.toRelative((IFile) resource);
                IPath copySource = HgStatusClient.getCopySource(root, path);

                // TODO: this approach doesn't work for "compare with current" action
                if (!path.equals(copySource)) {
                    resource = ResourceUtils.getFileHandle(root.toAbsolute(copySource));
                }
            }

            this.resource = resource;
        }
        hgRoot = root;
    }

    /**
     * @param hgRoot must be non null
     */
    public MercurialHistory(HgRoot hgRoot) {
        super();
        Assert.isNotNull(hgRoot);
        this.resource = null;
        this.hgRoot = hgRoot;
    }

    // operations

    /**
     * @return true if this is a history of the hg root, otherwise it's about any sibling of it
     */
    protected boolean isRootHistory() {
        return resource == null;
    }

    public void setBisectStarted(boolean started) {
        this.bisectStarted = started;
    }

    public boolean isBisectStarted() {
        return bisectStarted;
    }

    /**
     * @return last revision index requested for the current history, or zero if no
     *         revisions was requested.
     */
    public int getLastRequestedVersion() {
        return lastReqRevision;
    }

    public int getLastVersion() {
        if (revisions.isEmpty()) {
            return 0;
        }
        return revisions.get(revisions.size() - 1).getRevision();
    }

    public IFileRevision[] getContributors(IFileRevision revision) {
        return null;
    }

    public IFileRevision getFileRevision(String id) {
        if (revisions.isEmpty()) {
            return null;
        }

        for (MercurialRevision rev : revisions) {
            if (rev.getContentIdentifier().equals(id)) {
                return rev;
            }
        }
        return null;
    }

    public IFileRevision[] getFileRevisions() {
        if (!revisions.isEmpty()) {
            return revisions.toArray(new MercurialRevision[revisions.size()]);
        }
        return new IFileRevision[0];
    }

    public List<MercurialRevision> getRevisions() {
        if (!revisions.isEmpty()) {
            return new ArrayList<MercurialRevision>(revisions);
        }
        return Collections.emptyList();
    }

    public IFileRevision[] getTargets(IFileRevision revision) {
        return new IFileRevision[0];
    }

    /**
     * Load more revisions.
     *
     * @param monitor
     * @param from Revision number. The batch of revisions before this revision are loaded.
     * @throws CoreException
     */
    public void load(IProgressMonitor monitor, int from) throws CoreException {
        if (from < 0) {
            return;
        }
        if (from == Integer.MAX_VALUE) {
            // We're getting revisions up to the latest one available.
            // So clear out the cached list, as it may contain revisions
            // that no longer exist (e.g. after a strip/rollback).
            clear();
            tags = null;
            lastReqRevision = 0;
        }

        // check if we have reached the bottom (initially = Integer.MAX_VALUE)
        if (from == lastReqRevision) {
            return;
        }

        final IPreferenceStore store = MercurialEclipsePlugin.getDefault().getPreferenceStore();
        final int logBatchSize = store.getInt(LOG_BATCH_SIZE);
        final SortedSet<JHgChangeSet> changeSets = new TreeSet<JHgChangeSet>(CS_COMPARATOR);
        final IPath location;

        if (!isRootHistory()) {
            changeSets.addAll(HgLogClient.getResourceLog(hgRoot, resource, logBatchSize, from));
            location = ResourceUtils.getPath(resource);
        } else {
            changeSets.addAll(HgLogClient.getRootLog(hgRoot, logBatchSize, from));
            location = hgRoot.getIPath();
        }

        // no result -> bottom reached
        if (changeSets.isEmpty()) {
            lastReqRevision = from;
            return;
        }

        if (revisions.size() < changeSets.size()
                // ^ ????
                || !(location.equals(ResourceUtils.getPath(revisions.get(0).getResource())))) {
            clear();
        }

        List<MercurialRevision> batch = createMercurialRevisions(changeSets);

        if (showGraph) {
            try {
                loadGraphData(batch, revisions.isEmpty() ? null : revisions.get(revisions.size() - 1));
            } catch (Exception e) {
                // in some cases files that have been renamed cause loadGraphData to fail with an exception
                // catch it here so we can at least still display the non-graph revision info
                MercurialEclipsePlugin.logError("Failed to load graph data", e);
            }
        }

        if (!revisions.isEmpty()) {
            // in case of a particular data fetch before, we may still have some
            // temporary tags assigned to the last visible revision => cleanup it now
            MercurialRevision lastOne = revisions.get(revisions.size() - 1);
            lastOne.cleanupExtraTags();
        }

        int i = revisions.size();

        for (MercurialRevision rev : batch) {
            // When we follow renames, we are bound to have duplicate revisions (from both the new
            // and old names/paths). We make sure not to show the same revision several times.
            if (!revisions.contains(rev)) {

                if (layout != null) {
                    rev.setGraphRow(layout.getRow(i));
                }

                revisions.add(rev);
                i += 1;
            }
        }

        lastReqRevision = from;

        if (showTags) {
            if (!isRootHistory()) {
                if (tags == null) {
                    fetchTags();
                }
                assignTagsToRevisions();
            }
        }
    }

    private List<MercurialRevision> createMercurialRevisions(SortedSet<JHgChangeSet> changeSets)
            throws CoreException {
        IResource revisionResource = isRootHistory() ? hgRoot.getResource() : resource;
        Map<String, Signature> sigMap = getSignatures();
        Map<String, Status> bisectMap = HgBisectClient.getBisectStatus(hgRoot);
        setBisectStarted(!bisectMap.isEmpty());
        List<MercurialRevision> batch = new ArrayList<MercurialRevision>();

        for (JHgChangeSet cs : changeSets) {
            batch.add(new MercurialRevision(cs, revisionResource, sigMap.get(cs.getNode()),
                    bisectMap.get(cs.getNode())));
        }

        return batch;
    }

    private void loadGraphData(List<MercurialRevision> newRevs, MercurialRevision lastRev) {
        if (isRootHistory() || resource.getType() == IResource.FILE) {
            ParentProvider parentProvider;

            if (layout == null) {
                if (isRootHistory()) {
                    parentProvider = GraphLayout.ROOT_PARENT_PROVIDER;
                } else {
                    parentProvider = new FileParentProvider();
                }

                layout = new GraphLayout(parentProvider, GraphLogTableViewer.NUM_COLORS);
            } else {
                parentProvider = layout.getParentProvider();
            }

            Changeset[] changesets = new Changeset[newRevs.size()];
            int i = 0;
            for (MercurialRevision rev : newRevs) {
                changesets[i] = rev.getChangeSet().getData();
                i++;
            }

            if (parentProvider instanceof FileParentProvider) {
                ((FileParentProvider) parentProvider).prime(newRevs);
            }

            layout.add(changesets, lastRev == null ? null : lastRev.getChangeSet().getData());
        }
    }

    /**
     * Clear data
     */
    private void clear() {
        revisions.clear();
        layout = null;
        // TODO: tags?
    }

    private Map<String, Signature> getSignatures() throws CoreException {
        // get signatures
        Map<String, Signature> sigMap = new HashMap<String, Signature>();

        boolean sigcheck = "true".equals(HgClients.getPreference(PREF_SIGCHECK_IN_HISTORY, "false")); //$NON-NLS-2$

        if (sigcheck) {
            if (!"false".equals(MercurialUtilities.getGpgExecutable())) { //$NON-NLS-1$
                List<Signature> sigs = HgSigsClient.getSigs(hgRoot);
                for (Signature signature : sigs) {
                    sigMap.put(signature.getNodeId(), signature);
                }
            }
        }
        return sigMap;
    }

    private void fetchTags() {
        // we need extra tag changesets for files/folders only.
        Tag[] tags2 = HgTagClient.getTags(hgRoot);
        SortedSet<Tag> sorted = new TreeSet<Tag>();
        for (Tag tag : tags2) {
            if (!tag.isTip()) {
                sorted.add(tag);
            }
        }
        // tags are sorted naturally descending by cs revision
        tags = sorted.toArray(new Tag[sorted.size()]);
    }

    private void assignTagsToRevisions() {
        if (tags == null || tags.length == 0) {
            return;
        }
        int start = 0;
        // sorted ascending by revision
        for (Tag tag : tags) {
            int matchingRevision = getFirstMatchingRevision(tag, start);
            if (matchingRevision >= 0) {
                start = matchingRevision;
                revisions.get(matchingRevision).addTag(tag);
            }
        }
    }

    /**
     * TODO: rewrite so this is correct with non-linear graphs
     *
     * @param tag
     *            tag to search for
     * @param start
     *            start index in the revisions array
     * @return first matching revision index in the revisions array, or -1 if no one
     *         revision matches given tag
     */
    private int getFirstMatchingRevision(Tag tag, int start) {
        String tagBranch = tag.getChangeset().getBranch();
        int tagRev = tag.getChangeset().getRevision();
        // revisions are sorted descending by cs revision
        int lastRev = getLastRevision(tagBranch);
        for (int i = start; i <= lastRev; i++) {
            i = getNextRevision(i, tagBranch);
            int revision = revisions.get(i).getRevision();
            // perfect match
            if (revision == tagRev) {
                return i;
            }
            // if tag rev is greater as greatest (first) revision, return the version,
            // because the last file version was created before the tag => so it
            // was the current one at the time the tag was created
            if (i == 0 && tagRev > revision) {
                return i;
            }
            // if tag rev is smaller as smallest (last) revision, return
            if (i == lastRev && tagRev < revision) {
                // fix for bug 10830
                return -1;
            }
            // if tag rev is greater as current rev, return the version
            if (tagRev > revision) {
                return i;
            }
        }
        return -1;
    }

    /**
     * @param branch
     *            may be null
     * @return <b>internal index</b> of the latest revision known for this branch, or -1 if there
     *         are no matches
     */
    private int getLastRevision(String branch) {
        for (int i = revisions.size() - 1; i >= 0; i--) {
            MercurialRevision rev = revisions.get(i);
            if (BranchUtils.same(rev.getChangeSet().getBranch(), branch)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * @param from
     *            the first revision to start looking for
     * @param branch
     *            may be null
     * @return <b>internal index</b> of the next revision (starting from given one) known for this
     *         branch, or -1 if there are no matches
     */
    private int getNextRevision(int from, String branch) {
        for (int i = from; i < revisions.size(); i++) {
            MercurialRevision rev = revisions.get(i);
            if (BranchUtils.same(rev.getChangeSet().getBranch(), branch)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * @param showTags true to show tagged changesets, even if they are not related to the
     * current file
     */
    public void setEnableExtraTags(boolean showTags) {
        this.showTags = showTags;
    }

    /**
     * @param showGraph true to generate/report the extra information that is required to display
     * the revision graph
     */
    public void setEnableRevisionGraph(boolean showGraph) {
        this.showGraph = showGraph;
    }

    // inner types

    /**
     * Parent provider for a file following renames
     *
     * Strategy: Invoke parents for each of the known paths at each revision
     *
     * Using file status isn't sufficient: Copy source isn't present for some merges:
     * http://bz.selenic.com/show_bug.cgi?id=3495
     *
     * <pre>
     * hg log -Gf plugin/src/com/vectrace/MercurialEclipse/model/GChangeSet.java
     * between: a06450a60e5f and 2e26551ca397
     * </pre>
     */
    private class FileParentProvider implements ParentProvider {

        protected final Set<IPath> knownPaths = new HashSet<IPath>();

        private List<MercurialRevision> unknownPathRevs;

        // operations

        public void prime(final List<MercurialRevision> changesets) {
            unknownPathRevs = new LinkedList<MercurialRevision>(changesets);

            final IPath relativePath = hgRoot.getRelativePath(resource);
            String sRelativePath = relativePath.toString();

            knownPaths.add(relativePath);

            try {
                final GenericCommand command = new GenericCommand(hgRoot.getRepository(), "log");
                final File f = ResourceUtils.resourceAsFile("/styles/log_renames.tmpl");

                String[] s = new JavaHgCommandJob<String[]>(command, "Searching for file copies") {
                    @Override
                    protected String[] run() throws Exception {
                        return command.execute("--limit", Integer.toString(changesets.size()), "--style",
                                f.getPath(), relativePath.toString(), "--rev",
                                changesets.iterator().next().getRevision() + ":0").split("\0");
                    }
                }.execute(HgClients.getTimeOut(MercurialPreferenceConstants.LOG_TIMEOUT)).getValue();

                // -1 since there's a null at the end
                for (int i = 0, n = s.length - 1; i < n; i += 2) {
                    if (sRelativePath.equals(s[i])) {
                        knownPaths.add(hgRoot.toRelative(hgRoot.toAbsolute(s[i + 1])));
                    }
                }
            } catch (HgException e) {
                MercurialEclipsePlugin.logError(e);
            }
        }

        /**
         * @see com.vectrace.MercurialEclipse.history.GraphLayout.ParentProvider#getParents(com.aragost.javahg.Changeset)
         */
        public Changeset[] getParents(Changeset cs) {
            List<Changeset> parents = new ArrayList<Changeset>(4);

            for (IPath newPath : knownPaths) {
                try {
                    for (Changeset newChangeset : HgParentClient.getParents(hgRoot, cs, newPath)) {
                        if (!parents.contains(newChangeset)) {
                            parents.add(newChangeset);
                        }

                        setPath(newChangeset, newPath);
                    }
                } catch (ExecutionException e) {
                }
            }

            return parents.toArray(new Changeset[parents.size()]);
        }

        private void setPath(Changeset newChangeset, IPath newPath) {
            for (Iterator<MercurialRevision> it = unknownPathRevs.iterator(); it.hasNext();) {
                MercurialRevision rev = it.next();

                if (newChangeset.getRevision() == rev.getRevision()) {
                    rev.setIPath(newPath);
                    it.remove();
                    return;
                }
            }

            // Might happen if a file has two children
            // Eg exists under two names in the same revision
        }
    }
}