package br.uff.ic.dyevc.tools.vcs.git;
//~--- non-JDK imports --------------------------------------------------------
import br.uff.ic.dyevc.exception.DyeVCException;
import br.uff.ic.dyevc.model.CommitInfo;
//~--- JDK imports ------------------------------------------------------------
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Computes the common ancestor of the starting commits.
* <p>
* To compute the common ancestor a temporary flag is assigned to each of the starting commits. The maximum number of starting
* commits is bounded by the number of free flags available when the finder is initialized. These
* flags will be automatically released on the next reset of the finder, but not until then, as they are assigned to
* commits throughout the history.
* <p>
* Several internal flags are reused here for different purposes, but this should not have any impact as this finder
* should be run alone, and without any other finders wrapped around it.
*/
public class CommonAncestorFinder {
/**
* Set on {@link br.uff.ic.dyevc.model.CommitInfo} instances when they are first found in the commit history.
* <p>
* For a CommitInfo this indicates we have pulled apart the tree and parent references from the raw bytes available
* in the repository and translated those to our own local RevTree and RevCommit instances. The raw buffer is also
* available for message and other header filtering.
* <p>
* For a RevTag this indicates we have pulled part the tag references to find out who the tag refers to, and what
* that object's type is.
*/
static final int PARSED = 1 << 0;
/**
* Set on {@link br.uff.ic.dyevc.model.CommitInfo} instances added to our {@link #pending} queue.
* <p>
* We use this flag to avoid adding the same commit instance twice to our queue, especially if we reached it by more
* than one path.
*/
static final int SEEN = 1 << 1;
/**
* Set on {@link br.uff.ic.dyevc.model.CommitInfo} instances added to our {@link #pending} queue.
* <p>
* We use this flag to avoid adding the same commit instance twice to our queue, especially if we reached it by more
* than one path.
*/
private static final int IN_PENDING = 1 << 1;
/**
* Temporary mark for use within generators or filters.
* <p>
* This mark is only for local use within a single scope. If someone sets the mark they must unset it before any
* other code can see the mark.
*/
private static final int POPPED = 1 << 4;
/**
* Set on a {@link br.uff.ic.dyevc.model.CommitInfo} instances that can be a common ancestor.
*/
private static final int MERGE_BASE = 1 << 3;
/**
* Set on {@link br.uff.ic.dyevc.model.CommitInfo} instances the caller does not want output.
*/
static final int UNINTERESTING = 1 << 2;
/**
* Number of reserver flags that the application cannot use.
*/
static final int RESERVED_FLAGS = 6;
/**
* Flags that the application can use.
*/
private static final int APP_FLAGS = -1 & ~((1 << RESERVED_FLAGS) - 1);
/**
* Field description
*/
private int carryFlags = UNINTERESTING;
private int freeFlags = APP_FLAGS;
/**
* List of commits to start traversing from.
*/
private final ArrayList<CommitInfo> roots;
/**
* Initial queue of commits to start traversing from.
*/
private DateCommitQueue initQueue;
/**
* Queue of commits to be evaluated.
*/
private DateCommitQueue pending;
/**
* Map of {@link br.uff.ic.dyevc.model.CommitInfo} instances, keyed by their hash.
*/
private final Map<String, CommitInfo> commitMap;
/**
* Stores nodes that had flags changed, in order to reset them later;
*/
private HashSet<CommitInfo> changed;
private int branchMask;
private int recarryTest;
private int recarryMask;
private boolean initialized = false;
/**
* Constructs an instance of this finder.
* @param cis The map of {@link br.uff.ic.dyevc.model.CommitInfo} where the common ancestor
* will be searched.
*/
public CommonAncestorFinder(final Map<String, CommitInfo> cis) {
commitMap = cis;
pending = new DateCommitQueue();
initQueue = new DateCommitQueue();
roots = new ArrayList<CommitInfo>();
changed = new HashSet<CommitInfo>();
}
/**
* Initializes flags and pending queue based on a initial queue of commits.
* @param p The queue of commits to initialize this finder.
*/
private void init(DateCommitQueue p) {
try {
for (;;) {
final CommitInfo c = p.next();
if (c == null) {
break;
}
add(c);
}
} finally {
initialized = true;
// Always free the flags immediately. This ensures the flags
// will be available for reuse when the walk resets.
//
freeFlag(branchMask);
// Setup the condition used by carryOntoOne to detect a late
// merge base and produce it on the next round.
//
recarryTest = branchMask | POPPED;
recarryMask = branchMask | POPPED | MERGE_BASE;
}
}
/**
* Adds a commit in the pending queue and sets its flags
* @param c
*/
private void add(final CommitInfo c) {
final int flag = allocFlag();
branchMask |= flag;
if ((c.getFlags() & branchMask) != 0) {
// This should never happen. RevWalk ensures we get a
// commit admitted to the initial queue only once. If
// we see this marks aren't correctly erased.
//
throw new IllegalStateException(MessageFormat.format("Stale RevFlags on {0}", c.getHash()));
}
c.setFlags(c.getFlags() | flag);
changed.add(c);
pending.add(c);
}
/**
* Gets the common ancestor of the commits marked as start commits. Caller should previously
* invoke {@link #markStart(br.uff.ic.dyevc.model.CommitInfo) } method.
* @param revisions The revisions to find the common ancestor.
* @return The common ancestor for the commits marked as start commits.
*/
public CommitInfo getCommonAncestor(String... revisions) throws DyeVCException {
if (revisions == null) {
throw new DyeVCException("Revisions cannot be null.");
}
if (revisions.length == 0) {
throw new DyeVCException("Revisions cannot be empty.");
}
for (String revision : revisions) {
markStart(commitMap.get(revision));
}
// Initializes pending queue based on initQueue (nodes marked to start from)
if (!initialized) {
init(initQueue);
}
for (;;) {
final CommitInfo c = pending.next();
if (c == null) {
reset();
return null;
}
for (final String id : c.getParents()) {
final CommitInfo p = commitMap.get(id);
if ((p.getFlags() & IN_PENDING) != 0) {
continue;
}
if ((c.getFlags() & PARSED) == 0) {
c.markInWalk();
c.setFlags(c.getFlags() | PARSED);
changed.add(c);
}
p.setFlags(p.getFlags() | IN_PENDING);
changed.add(p);
pending.add(p);
}
int carry = c.getFlags() & branchMask;
boolean mb = carry == branchMask;
if (mb) {
// If we are a merge base make sure our ancestors are
// also flagged as being popped, so that they do not
// generate to the caller.
//
carry |= MERGE_BASE;
}
carryOntoHistory(c, carry);
if ((c.getFlags() & MERGE_BASE) != 0) {
// This commit is an ancestor of a merge base we already
// popped back to the caller. If everyone in pending is
// that way we are done traversing; if not we just need
// to move to the next available commit and try again.
//
if (pending.everbodyHasFlag(MERGE_BASE)) {
reset();
return null;
}
continue;
}
c.setFlags(c.getFlags() | POPPED);
changed.add(c);
if (mb) {
// c.setFlags(c.getFlags() | MERGE_BASE);
reset();
return c;
}
}
}
private void carryOntoHistory(CommitInfo c, final int carry) {
for (;;) {
final Set<String> parents = c.getParents();
if (parents == null) {
return;
}
final String[] pList = parents.toArray(new String[0]);
if (pList == null) {
return;
}
final int n = pList.length;
if (n == 0) {
return;
}
for (int i = 1; i < n; i++) {
final CommitInfo p = commitMap.get(pList[i]);
if (!carryOntoOne(p, carry)) {
carryOntoHistory(p, carry);
}
}
c = commitMap.get(pList[0]);
if (carryOntoOne(c, carry)) {
break;
}
}
}
private boolean carryOntoOne(final CommitInfo p, final int carry) {
final boolean haveAll = (p.getFlags() & carry) == carry;
p.setFlags(p.getFlags() | carry);
changed.add(p);
if ((p.getFlags() & recarryMask) == recarryTest) {
// We were popped without being a merge base, but we just got
// voted to be one. Inject ourselves back at the front of the
// pending queue and tell all of our ancestors they are within
// the merge base now.
//
p.setFlags(p.getFlags() & ~POPPED);
changed.add(p);
pending.add(p);
carryOntoHistory(p, branchMask | MERGE_BASE);
return true;
}
// If we already had all carried flags, our parents do too.
// Return true to stop the caller from running down this leg
// of the revision graph any further.
//
return haveAll;
}
/**
* Method description
*
*
* @param mask
*/
void freeFlag(final int mask) {
freeFlags |= mask;
carryFlags &= ~mask;
}
/**
* Method description
*
*
* @return
*/
int allocFlag() {
if (freeFlags == 0) {
throw new IllegalArgumentException(MessageFormat.format("{0} flags already created",
Integer.valueOf(32 - RESERVED_FLAGS)));
}
final int m = Integer.lowestOneBit(freeFlags);
freeFlags &= ~m;
return m;
}
/**
* Mark a commit to start graph traversal from.
*
* @param c the commit to start traversing from. The commit passed must exist in the {@link #commitMap} informed
* on the constructor.
*/
public final void markStart(final CommitInfo c) {
if ((c.getFlags() & SEEN) != 0) {
return;
}
if ((c.getFlags() & PARSED) == 0) {
c.markInWalk();
c.setFlags(c.getFlags() | PARSED);
changed.add(c);
}
c.setFlags(c.getFlags() | SEEN);
changed.add(c);
roots.add(c);
initQueue.add(c);
}
/**
* Resets internal state of all changed commits and allows this instance to be used again.
*/
protected void reset() {
for (CommitInfo c : changed) {
c.resetWalk();
}
roots.clear();
changed.clear();
initQueue = new DateCommitQueue();
pending = new DateCommitQueue();
freeFlags = APP_FLAGS;
carryFlags = UNINTERESTING;
initialized = false;
}
}