Gitnuro/src/main/kotlin/app/git/graph/GraphCommitList.kt
Abdelilah El Aissaoui 00d95b6feb Project code cleanup
2022-08-06 21:03:45 +02:00

363 lines
13 KiB
Kotlin

package app.git.graph
import org.eclipse.jgit.internal.JGitText
import org.eclipse.jgit.lib.AnyObjectId
import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.revwalk.RevCommitList
import org.eclipse.jgit.revwalk.RevWalk
import java.text.MessageFormat
import java.util.*
/**
* An ordered list of [GraphNode] subclasses.
*
*
* Commits are allocated into lanes as they enter the list, based upon their
* connections between descendant (child) commits and ancestor (parent) commits.
*
*
* The source of the list must be a [GraphWalk]
* and [.fillTo] must be used to populate the list.
*
* type of lane used by the application.
</L> */
class GraphCommitList : RevCommitList<GraphNode>() {
private var positionsAllocated = 0
private val freePositions = TreeSet<Int>()
private val activeLanes = HashSet<GraphLane>(32)
var maxLine = 0
private set
/** number of (child) commits on a lane */
private val laneLength = HashMap<GraphLane, Int?>(
32
)
override fun clear() {
super.clear()
positionsAllocated = 0
freePositions.clear()
activeLanes.clear()
laneLength.clear()
}
override fun source(revWalk: RevWalk) {
if (revWalk !is GraphWalk) throw ClassCastException(
MessageFormat.format(
JGitText.get().classCastNotA,
GraphWalk::class.java.name
)
)
super.source(revWalk)
}
private var parentId: AnyObjectId? = null
private val graphCommit = UncommitedChangesGraphNode()
fun addUncommitedChangesGraphCommit(parent: RevCommit) {
parentId = parent.id
graphCommit.lane = nextFreeLane()
}
override fun enter(index: Int, currCommit: GraphNode) {
var isUncommitedChangesNodeParent = false
if (currCommit.id == parentId) {
graphCommit.graphParent = currCommit
currCommit.addChild(graphCommit, addFirst = true)
isUncommitedChangesNodeParent = true
}
setupChildren(currCommit)
val nChildren = currCommit.childCount
if (nChildren == 0) {
currCommit.lane = nextFreeLane()
} else if (nChildren == 1
&& currCommit.children[0].graphParentCount < 2
) {
// Only one child, child has only us as their parent.
// Stay in the same lane as the child.
val graphNode: GraphNode = currCommit.children[0]
currCommit.lane = graphNode.lane
var len = laneLength[currCommit.lane]
len = if (len != null) Integer.valueOf(len.toInt() + 1) else Integer.valueOf(0)
if (currCommit.lane.position != INVALID_LANE_POSITION)
laneLength[currCommit.lane] = len
} else {
// 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:
var reservedLane: GraphLane? = null
var childOnReservedLane: GraphNode? = null
var lengthOfReservedLane = -1
if (isUncommitedChangesNodeParent) {
val length = laneLength[graphCommit.lane]
if (length != null) {
reservedLane = graphCommit.lane
childOnReservedLane = graphCommit
lengthOfReservedLane = length
}
} else {
for (i in 0 until nChildren) {
val c: GraphNode = currCommit.children[i]
if (c.getGraphParent(0) === currCommit) {
if (c.lane.position < 0)
println("c.lane.position is invalid (${c.lane.position})")
val length = laneLength[c.lane]
// we may be the first parent for multiple lines of
// development, try to continue the longest one
if (length != null && length > lengthOfReservedLane) {
reservedLane = c.lane
childOnReservedLane = c
lengthOfReservedLane = length
}
}
}
}
if (reservedLane != null) {
currCommit.lane = reservedLane
laneLength[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 (i in 0 until nChildren) {
val graphNode = currCommit.children[i]
val firstParent = graphNode.getGraphParent(0)
if (firstParent.lane.position != INVALID_LANE_POSITION && firstParent.lane !== graphNode.lane)
closeLane(graphNode.lane)
}
}
continueActiveLanes(currCommit)
if (currCommit.parentCount == 0 && currCommit.lane.position == INVALID_LANE_POSITION)
closeLane(currCommit.lane)
}
private fun continueActiveLanes(currCommit: GraphNode) {
for (lane in 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 `currCommit` in the list
* @param currentNode
* @param childOnLane
* the direct child on the same lane as `currCommit`,
* may be null if `currCommit` is the first commit on
* the lane
*/
private fun handleBlockedLanes(
index: Int, currentNode: GraphNode,
childOnLane: GraphNode?
) {
for (child in currentNode.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?
val childIsMerge = child.getGraphParent(0) !== currentNode
if (childIsMerge) {
var laneToUse = currentNode.lane
laneToUse = handleMerge(
index, currentNode, 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.
val laneToUse = child.lane
currentNode.addForkingOffLane(laneToUse)
}
}
}
// Handles the case where currCommit is a non-first parent of the child
private fun handleMerge(
index: Int, currCommit: GraphNode,
childOnLane: GraphNode?, child: GraphNode, laneToUse: GraphLane
): GraphLane {
// find all blocked positions between currCommit and this child
var newLaneToUse = laneToUse
var childIndex = index // useless initialization, should
// always be set in the loop below
val blockedPositions = BitSet()
for (r in index - 1 downTo 0) {
val graphNode: GraphNode? = get(r)
if (graphNode === child) {
childIndex = r
break
}
addBlockedPosition(blockedPositions, graphNode)
}
// handle blockades
if (blockedPositions[newLaneToUse.position]) {
// 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
var needDetour = false
if (childOnLane != null) {
for (r in index - 1 downTo childIndex + 1) {
val graphNode: GraphNode? = get(r)
if (graphNode === 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.
newLaneToUse = nextFreeLane(blockedPositions)
currCommit.addForkingOffLane(newLaneToUse)
closeLane(newLaneToUse)
} 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.
val newPos = getFreePosition(blockedPositions)
freePositions.add(newLaneToUse.position)
newLaneToUse.position = newPos
}
}
// Actually connect currCommit to the merge child
drawLaneToChild(index, child, newLaneToUse)
return newLaneToUse
}
/**
* 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 fun drawLaneToChild(
commitIndex: Int, child: GraphNode,
laneToContinue: GraphLane
) {
for (index in commitIndex - 1 downTo 0) {
val graphNode: GraphNode? = get(index)
if (graphNode === child) break
graphNode?.addPassingLane(laneToContinue)
}
}
private fun closeLane(lane: GraphLane) {
if (activeLanes.remove(lane)) {
laneLength.remove(lane)
freePositions.add(Integer.valueOf(lane.position))
}
}
private fun setupChildren(currCommit: GraphNode) {
val nParents = currCommit.parentCount
for (i in 0 until nParents) (currCommit.getParent(i) as GraphNode).addChild(currCommit)
}
private fun nextFreeLane(blockedPositions: BitSet? = null): GraphLane {
val newPlotLane = GraphLane(position = getFreePosition(blockedPositions))
activeLanes.add(newPlotLane)
laneLength[newPlotLane] = Integer.valueOf(1)
return newPlotLane
}
/**
* @param blockedPositions
* may be null
* @return a free lane position
*/
private fun getFreePosition(blockedPositions: BitSet?): Int {
if (freePositions.isEmpty()) return positionsAllocated++
if (blockedPositions != null) {
for (pos in freePositions) if (!blockedPositions[pos]) {
freePositions.remove(pos)
return pos
}
return positionsAllocated++
}
val min = freePositions.first()
freePositions.remove(min)
return min.toInt()
}
private fun addBlockedPosition(
blockedPositions: BitSet,
graphNode: GraphNode?
) {
if (graphNode != null) {
val lane = graphNode.lane
// Positions may be blocked by a commit on a lane.
if (lane.position != INVALID_LANE_POSITION) {
blockedPositions.set(lane.position)
}
// 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 (graphLane in graphNode.forkingOffLanes) {
blockedPositions.set(graphLane.position)
}
for (graphLane in graphNode.mergingLanes) {
blockedPositions.set(graphLane.position)
}
}
}
fun calcMaxLine() {
if (this.isNotEmpty()) {
maxLine = this.maxOf { it.lane.position }
}
}
}