org.eclipse.jgit.revplot.PlotCommitList.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jgit.revplot.PlotCommitList.java

Source

/*
 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>,
 * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
 * Copyright (C) 2014, Konrad Kgler
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.eclipse.jgit.revplot;

import java.text.MessageFormat;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.TreeSet;

import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.revwalk.RevCommitList;
import org.eclipse.jgit.revwalk.RevWalk;

/**
 * An ordered list of {@link org.eclipse.jgit.revplot.PlotCommit} subclasses.
 * <p>
 * Commits are allocated into lanes as they enter the list, based upon their
 * connections between descendant (child) commits and ancestor (parent) commits.
 * <p>
 * The source of the list must be a {@link org.eclipse.jgit.revplot.PlotWalk}
 * and {@link #fillTo(int)} must be used to populate the list.
 *
 * @param <L>
 *            type of lane used by the application.
 */
public class PlotCommitList<L extends PlotLane> extends RevCommitList<PlotCommit<L>> {
    static final int MAX_LENGTH = 25;

    private int positionsAllocated;

    private final TreeSet<Integer> freePositions = new TreeSet<>();

    private final HashSet<PlotLane> activeLanes = new HashSet<>(32);

    /** number of (child) commits on a lane */
    private final HashMap<PlotLane, Integer> laneLength = new HashMap<>(32);

    /** {@inheritDoc} */
    @Override
    public void clear() {
        super.clear();
        positionsAllocated = 0;
        freePositions.clear();
        activeLanes.clear();
        laneLength.clear();
    }

    /** {@inheritDoc} */
    @Override
    public void source(RevWalk w) {
        if (!(w instanceof PlotWalk))
            throw new ClassCastException(
                    MessageFormat.format(JGitText.get().classCastNotA, PlotWalk.class.getName()));
        super.source(w);
    }

    /**
     * Find the set of lanes passing through a commit's row.
     * <p>
     * Lanes passing through a commit are lanes that the commit is not directly
     * on, but that need to travel through this commit to connect a descendant
     * (child) commit to an ancestor (parent) commit. Typically these lanes will
     * be drawn as lines in the passed commit's box, and the passed commit won't
     * appear to be connected to those lines.
     * <p>
     * This method modifies the passed collection by adding the lanes in any
     * order.
     *
     * @param currCommit
     *            the commit the caller needs to get the lanes from.
     * @param result
     *            collection to add the passing lanes into.
     */
    @SuppressWarnings("unchecked")
    public void findPassingThrough(final PlotCommit<L> currCommit, final Collection<L> result) {
        for (PlotLane p : currCommit.passingLanes)
            result.add((L) p);
    }

    /** {@inheritDoc} */
    @Override
    protected void enter(int index, PlotCommit<L> currCommit) {
        setupChildren(currCommit);

        final int nChildren = currCommit.getChildCount();
        if (nChildren == 0) {
            currCommit.lane = nextFreeLane();
        } else if (nChildren == 1 && currCommit.children[0].getParentCount() < 2) {
            // Only one child, child has only us as their parent.
            // Stay in the same lane as the child.

            @SuppressWarnings("unchecked")
            final PlotCommit<L> c = currCommit.children[0];
            currCommit.lane = c.lane;
            Integer len = laneLength.get(currCommit.lane);
            len = len != null ? Integer.valueOf(len.intValue() + 1) : Integer.valueOf(0);
            laneLength.put(currCommit.lane, len);
        } else {
            // More than one child, or our child is a merge.

            // We look for the child lane the current commit should continue.
            // Candidate lanes for this are those with children, that have the
            // current commit as their first parent.
            // There can be multiple candidate lanes. In that case the longest
            // lane is chosen, as this is usually the lane representing the
            // branch the commit actually was made on.

            // When there are no candidate lanes (i.e. the current commit has
            // only children whose non-first parent it is) we place the current
            // commit on a new lane.

            // The lane the current commit will be placed on:
            PlotLane reservedLane = null;
            PlotCommit childOnReservedLane = null;
            int lengthOfReservedLane = -1;

            for (int i = 0; i < nChildren; i++) {
                @SuppressWarnings("unchecked")
                final PlotCommit<L> c = currCommit.children[i];
                if (c.getParent(0) == currCommit) {
                    Integer len = laneLength.get(c.lane);
                    // we may be the first parent for multiple lines of
                    // development, try to continue the longest one
                    if (len.intValue() > lengthOfReservedLane) {
                        reservedLane = c.lane;
                        childOnReservedLane = c;
                        lengthOfReservedLane = len.intValue();
                    }
                }
            }

            if (reservedLane != null) {
                currCommit.lane = reservedLane;
                laneLength.put(reservedLane, Integer.valueOf(lengthOfReservedLane + 1));
                handleBlockedLanes(index, currCommit, childOnReservedLane);
            } else {
                currCommit.lane = nextFreeLane();
                handleBlockedLanes(index, currCommit, null);
            }

            // close lanes of children, if there are no first parents that might
            // want to continue the child lanes
            for (int i = 0; i < nChildren; i++) {
                final PlotCommit c = currCommit.children[i];
                PlotCommit firstParent = (PlotCommit) c.getParent(0);
                if (firstParent.lane != null && firstParent.lane != c.lane)
                    closeLane(c.lane);
            }
        }

        continueActiveLanes(currCommit);
        if (currCommit.getParentCount() == 0)
            closeLane(currCommit.lane);
    }

    private void continueActiveLanes(PlotCommit currCommit) {
        for (PlotLane lane : activeLanes)
            if (lane != currCommit.lane)
                currCommit.addPassingLane(lane);
    }

    /**
     * Sets up fork and merge information in the involved PlotCommits.
     * Recognizes and handles blockades that involve forking or merging arcs.
     *
     * @param index
     *            the index of <code>currCommit</code> in the list
     * @param currCommit
     * @param childOnLane
     *            the direct child on the same lane as <code>currCommit</code>,
     *            may be null if <code>currCommit</code> is the first commit on
     *            the lane
     */
    private void handleBlockedLanes(final int index, final PlotCommit currCommit, final PlotCommit childOnLane) {
        for (PlotCommit child : currCommit.children) {
            if (child == childOnLane)
                continue; // simple continuations of lanes are handled by
                          // continueActiveLanes() calls in enter()

            // Is the child a merge or is it forking off?
            boolean childIsMerge = child.getParent(0) != currCommit;
            if (childIsMerge) {
                PlotLane laneToUse = currCommit.lane;
                laneToUse = handleMerge(index, currCommit, childOnLane, child, laneToUse);
                child.addMergingLane(laneToUse);
            } else {
                // We want to draw a forking arc in the child's lane.
                // As an active lane, the child lane already continues
                // (unblocked) up to this commit, we only need to mark it as
                // forking off from the current commit.
                PlotLane laneToUse = child.lane;
                currCommit.addForkingOffLane(laneToUse);
            }
        }
    }

    // Handles the case where currCommit is a non-first parent of the child
    private PlotLane handleMerge(final int index, final PlotCommit currCommit, final PlotCommit childOnLane,
            PlotCommit child, PlotLane laneToUse) {

        // find all blocked positions between currCommit and this child

        int childIndex = index; // useless initialization, should
        // always be set in the loop below
        BitSet blockedPositions = new BitSet();
        for (int r = index - 1; r >= 0; r--) {
            final PlotCommit rObj = get(r);
            if (rObj == child) {
                childIndex = r;
                break;
            }
            addBlockedPosition(blockedPositions, rObj);
        }

        // handle blockades

        if (blockedPositions.get(laneToUse.getPosition())) {
            // We want to draw a merging arc in our lane to the child,
            // which is on another lane, but our lane is blocked.

            // Check if childOnLane is beetween commit and the child we
            // are currently processing
            boolean needDetour = false;
            if (childOnLane != null) {
                for (int r = index - 1; r > childIndex; r--) {
                    final PlotCommit rObj = get(r);
                    if (rObj == childOnLane) {
                        needDetour = true;
                        break;
                    }
                }
            }

            if (needDetour) {
                // It is childOnLane which is blocking us. Repositioning
                // our lane would not help, because this repositions the
                // child too, keeping the blockade.
                // Instead, we create a "detour lane" which gets us
                // around the blockade. That lane has no commits on it.
                laneToUse = nextFreeLane(blockedPositions);
                currCommit.addForkingOffLane(laneToUse);
                closeLane(laneToUse);
            } else {
                // The blockade is (only) due to other (already closed)
                // lanes at the current lane's position. In this case we
                // reposition the current lane.
                // We are the first commit on this lane, because
                // otherwise the child commit on this lane would have
                // kept other lanes from blocking us. Since we are the
                // first commit, we can freely reposition.
                int newPos = getFreePosition(blockedPositions);
                freePositions.add(Integer.valueOf(laneToUse.getPosition()));
                laneToUse.position = newPos;
            }
        }

        // Actually connect currCommit to the merge child
        drawLaneToChild(index, child, laneToUse);
        return laneToUse;
    }

    /**
     * Connects the commit at commitIndex to the child, using the given lane.
     * All blockades on the lane must be resolved before calling this method.
     *
     * @param commitIndex
     * @param child
     * @param laneToContinue
     */
    private void drawLaneToChild(final int commitIndex, PlotCommit child, PlotLane laneToContinue) {
        for (int r = commitIndex - 1; r >= 0; r--) {
            final PlotCommit rObj = get(r);
            if (rObj == child)
                break;
            if (rObj != null)
                rObj.addPassingLane(laneToContinue);
        }
    }

    private static void addBlockedPosition(BitSet blockedPositions, final PlotCommit rObj) {
        if (rObj != null) {
            PlotLane lane = rObj.getLane();
            // Positions may be blocked by a commit on a lane.
            if (lane != null)
                blockedPositions.set(lane.getPosition());
            // Positions may also be blocked by forking off and merging lanes.
            // We don't consider passing lanes, because every passing lane forks
            // off and merges at it ends.
            for (PlotLane l : rObj.forkingOffLanes)
                blockedPositions.set(l.getPosition());
            for (PlotLane l : rObj.mergingLanes)
                blockedPositions.set(l.getPosition());
        }
    }

    @SuppressWarnings("unchecked")
    private void closeLane(PlotLane lane) {
        if (activeLanes.remove(lane)) {
            recycleLane((L) lane);
            laneLength.remove(lane);
            freePositions.add(Integer.valueOf(lane.getPosition()));
        }
    }

    private void setupChildren(PlotCommit<L> currCommit) {
        final int nParents = currCommit.getParentCount();
        for (int i = 0; i < nParents; i++)
            ((PlotCommit) currCommit.getParent(i)).addChild(currCommit);
    }

    private PlotLane nextFreeLane() {
        return nextFreeLane(null);
    }

    private PlotLane nextFreeLane(BitSet blockedPositions) {
        final PlotLane p = createLane();
        p.position = getFreePosition(blockedPositions);
        activeLanes.add(p);
        laneLength.put(p, Integer.valueOf(1));
        return p;
    }

    /**
     * @param blockedPositions
     *            may be null
     * @return a free lane position
     */
    private int getFreePosition(BitSet blockedPositions) {
        if (freePositions.isEmpty())
            return positionsAllocated++;

        if (blockedPositions != null) {
            for (Integer pos : freePositions)
                if (!blockedPositions.get(pos.intValue())) {
                    freePositions.remove(pos);
                    return pos.intValue();
                }
            return positionsAllocated++;
        }
        final Integer min = freePositions.first();
        freePositions.remove(min);
        return min.intValue();
    }

    /**
     * Create a new {@link PlotLane} appropriate for this particular
     * {@link PlotCommitList}.
     *
     * @return a new {@link PlotLane} appropriate for this particular
     *         {@link PlotCommitList}.
     */
    @SuppressWarnings("unchecked")
    protected L createLane() {
        return (L) new PlotLane();
    }

    /**
     * Return colors and other reusable information to the plotter when a lane
     * is no longer needed.
     *
     * @param lane
     *            a lane
     */
    protected void recycleLane(L lane) {
        // Nothing.
    }
}