Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
|
a7d40638ab | ||
|
1476295415 | ||
|
2c8f8da9d8 |
21 changed files with 471 additions and 879 deletions
|
@ -101,7 +101,7 @@ kotlin {
|
|||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions.allWarningsAsErrors = true
|
||||
kotlinOptions.allWarningsAsErrors = false
|
||||
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||
}
|
||||
|
||||
|
|
|
@ -8,13 +8,13 @@ crate-type = ["cdylib"]
|
|||
name = "gitnuro_rs"
|
||||
|
||||
[dependencies]
|
||||
uniffi = { version = "0.26.0" }
|
||||
notify = "6.1.1"
|
||||
thiserror = "1.0.56"
|
||||
uniffi = { version = "0.25.0" }
|
||||
notify = "6.0.1"
|
||||
thiserror = "1.0.43"
|
||||
libssh-rs = { version = "0.2.2", features = ["vendored", "vendored-openssl"] }
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { version = "0.26.0", features = ["build"] }
|
||||
uniffi = { version = "0.25.0", features = ["build"] }
|
||||
|
||||
[[bin]]
|
||||
name = "uniffi-bindgen"
|
||||
|
|
|
@ -61,7 +61,7 @@ val Ref.remoteName: String
|
|||
|
||||
val Ref.isBranch: Boolean
|
||||
get() {
|
||||
return this is ObjectIdRef.PeeledNonTag
|
||||
return this.name.startsWith(Constants.R_HEADS) || this.name.startsWith(Constants.R_REMOTES)
|
||||
}
|
||||
|
||||
val Ref.isHead: Boolean
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
package com.jetpackduba.gitnuro.git.graph
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class GenerateLogWalkUseCase @Inject constructor() {
|
||||
suspend operator fun invoke(
|
||||
git: Git,
|
||||
firstCommit: RevCommit,
|
||||
allRefs: List<Ref>,
|
||||
stashes: List<RevCommit>,
|
||||
hasUncommittedChanges: Boolean,
|
||||
commitsLimit: Int?,
|
||||
): GraphCommitList2 = withContext(Dispatchers.IO) {
|
||||
val reservedLanes = mutableMapOf<Int, String>()
|
||||
val graphNodes = mutableListOf<GraphNode2>()
|
||||
var maxLane = 0
|
||||
|
||||
val availableCommitsToAdd = mutableMapOf<String, RevCommit>()
|
||||
|
||||
val refsWithCommits = allRefs.map {
|
||||
val commit = git.repository.parseCommit(it.objectId)
|
||||
|
||||
commit to it
|
||||
}
|
||||
|
||||
val commitsOfRefs = refsWithCommits.map {
|
||||
it.first.name to it.first
|
||||
}
|
||||
|
||||
val commitsOfStashes = stashes.map { it.name to it }
|
||||
|
||||
availableCommitsToAdd[firstCommit.name] = firstCommit
|
||||
availableCommitsToAdd.putAll(commitsOfRefs)
|
||||
availableCommitsToAdd.putAll(commitsOfStashes)
|
||||
|
||||
var currentCommit = getNextCommit(availableCommitsToAdd.values.toList())
|
||||
|
||||
if (hasUncommittedChanges) {
|
||||
reservedLanes[0] = firstCommit.name
|
||||
}
|
||||
|
||||
availableCommitsToAdd.remove(currentCommit?.name)
|
||||
|
||||
while (currentCommit != null && (commitsLimit == null || graphNodes.count() <= commitsLimit)) {
|
||||
val lanes = getReservedLanes(reservedLanes, currentCommit.name)
|
||||
val lane = lanes.first()
|
||||
val forkingLanes = lanes - lane
|
||||
val isStash = stashes.any { it == currentCommit }
|
||||
|
||||
val parents = sortParentsByPriority(git, currentCommit)
|
||||
.filterStashParentsIfRequired(isStash)
|
||||
|
||||
val parentsCount = parents.count()
|
||||
val mergingLanes = mutableListOf<Int>()
|
||||
if (parentsCount == 1) {
|
||||
reservedLanes[lane] = parents.first().name
|
||||
} else if (parentsCount > 1) {
|
||||
reservedLanes[lane] = parents.first().name
|
||||
|
||||
for (i in 1 until parentsCount) {
|
||||
val availableLane = firstAvailableLane(reservedLanes)
|
||||
reservedLanes[availableLane] = parents[i].name
|
||||
mergingLanes.add(availableLane)
|
||||
}
|
||||
}
|
||||
|
||||
val refs = refsByCommit(refsWithCommits, currentCommit)
|
||||
|
||||
val graphNode = createGraphNode(
|
||||
currentCommit = currentCommit,
|
||||
isStash = isStash,
|
||||
lane = lane,
|
||||
forkingLanes = forkingLanes,
|
||||
reservedLanes = reservedLanes,
|
||||
mergingLanes = mergingLanes,
|
||||
refs = refs
|
||||
)
|
||||
|
||||
if (lane > maxLane) {
|
||||
maxLane = lane
|
||||
}
|
||||
|
||||
graphNodes.add(graphNode)
|
||||
removeFromAllLanes(reservedLanes, graphNode.name)
|
||||
|
||||
availableCommitsToAdd.putAll(parents.map { it.name to it })
|
||||
|
||||
currentCommit = getNextCommit(availableCommitsToAdd.values.toList())
|
||||
availableCommitsToAdd.remove(currentCommit?.name)
|
||||
}
|
||||
|
||||
GraphCommitList2(graphNodes, maxLane)
|
||||
}
|
||||
|
||||
private fun createGraphNode(
|
||||
currentCommit: RevCommit,
|
||||
isStash: Boolean,
|
||||
lane: Int,
|
||||
forkingLanes: List<Int>,
|
||||
reservedLanes: MutableMap<Int, String>,
|
||||
mergingLanes: MutableList<Int>,
|
||||
refs: List<Ref>
|
||||
) = GraphNode2(
|
||||
currentCommit.name,
|
||||
currentCommit.shortMessage,
|
||||
currentCommit.fullMessage,
|
||||
currentCommit.authorIdent,
|
||||
currentCommit.committerIdent,
|
||||
currentCommit.parentCount,
|
||||
isStash = isStash,
|
||||
lane = lane,
|
||||
forkingLanes = forkingLanes,
|
||||
passingLanes = reservedLanes.keys.toList() - mergingLanes.toSet() - forkingLanes.toSet(),
|
||||
mergingLanes = mergingLanes,
|
||||
refs = refs,
|
||||
)
|
||||
|
||||
private fun refsByCommit(
|
||||
refsWithCommits: List<Pair<RevCommit, Ref>>,
|
||||
commit: RevCommit
|
||||
): List<Ref> = refsWithCommits
|
||||
.filter { it.first == commit }
|
||||
.map { it.second }
|
||||
|
||||
private fun sortParentsByPriority(git: Git, currentCommit: RevCommit): List<RevCommit> {
|
||||
val parents = currentCommit
|
||||
.parents
|
||||
.map { git.repository.parseCommit(it) }
|
||||
.toMutableList()
|
||||
|
||||
return if (parents.count() <= 1) {
|
||||
parents
|
||||
} else if (parents.count() == 2) {
|
||||
if (parents[0].parents.any { it.name == parents[1].name }) {
|
||||
listOf(parents[1], parents[0])
|
||||
} else if (parents[1].parents.any { it.name == parents[0].name }) {
|
||||
parents
|
||||
} else {
|
||||
parents // TODO Sort by longer tree or detect the origin branch
|
||||
}
|
||||
} else {
|
||||
parents.sortedBy { it.committerIdent.`when` }
|
||||
}
|
||||
}
|
||||
|
||||
fun List<RevCommit>.filterStashParentsIfRequired(isStash: Boolean): List<RevCommit> {
|
||||
return if (isStash) {
|
||||
filterNot {
|
||||
it.shortMessage.startsWith("index on") ||
|
||||
it.shortMessage.startsWith("untracked files on")
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
fun getNextCommit(availableCommits: List<RevCommit>): RevCommit? {
|
||||
return availableCommits.sortedByDescending { it.committerIdent.`when` }.firstOrNull()
|
||||
}
|
||||
|
||||
fun getReservedLanes(reservedLanes: Map<Int, String>, hash: String): List<Int> {
|
||||
val reservedLanesFiltered = reservedLanes.entries
|
||||
.asSequence()
|
||||
.map { it.key to it.value }
|
||||
.filter { (key, value) -> value == hash }
|
||||
.sortedBy { it.first }
|
||||
.toList()
|
||||
|
||||
return if (reservedLanesFiltered.isEmpty()) {
|
||||
listOf(firstAvailableLane(reservedLanes))
|
||||
} else {
|
||||
reservedLanesFiltered.map { it.first }
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromAllLanes(reservedLanes: MutableMap<Int, String>, hash: String) {
|
||||
val lanes = reservedLanes.entries.filter { it.value == hash }
|
||||
|
||||
for (lane in lanes) {
|
||||
reservedLanes.remove(lane.key)
|
||||
}
|
||||
}
|
||||
|
||||
fun firstAvailableLane(reservedLanes: Map<Int, String>): Int {
|
||||
val sortedKeys = reservedLanes.keys.sorted()
|
||||
|
||||
return if (sortedKeys.isEmpty() || sortedKeys.first() > 0) {
|
||||
0
|
||||
} else if (sortedKeys.count() == 1) {
|
||||
val first = sortedKeys.first()
|
||||
|
||||
if (first == 0) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
sortedKeys.asSequence()
|
||||
.zipWithNext { a, b ->
|
||||
if (b - a > 1)
|
||||
a + 1
|
||||
else
|
||||
null
|
||||
}
|
||||
.filterNotNull()
|
||||
.firstOrNull() ?: (sortedKeys.max() + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ReservationType {
|
||||
val hash: String
|
||||
|
||||
class ParentInSameLane(override val hash: String): ReservationType
|
||||
class ParentInVariableLane(override val hash: String): ReservationType
|
||||
}
|
|
@ -1,366 +0,0 @@
|
|||
package com.jetpackduba.gitnuro.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 = UncommittedChangesGraphNode()
|
||||
|
||||
fun addUncommittedChangesGraphCommit(parent: RevCommit) {
|
||||
parentId = parent.id
|
||||
graphCommit.lane = nextFreeLane()
|
||||
}
|
||||
|
||||
override fun enter(index: Int, currCommit: GraphNode) {
|
||||
var isUncommittedChangesNodeParent = false
|
||||
if (currCommit.id == parentId) {
|
||||
graphCommit.graphParent = currCommit
|
||||
currCommit.addChild(graphCommit, addFirst = true)
|
||||
isUncommittedChangesNodeParent = 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 (isUncommittedChangesNodeParent) {
|
||||
val length = laneLength[graphCommit.lane]
|
||||
if (length != null) {
|
||||
reservedLane = graphCommit.lane
|
||||
childOnReservedLane = graphCommit
|
||||
lengthOfReservedLane = length
|
||||
}
|
||||
} else {
|
||||
val children = currCommit.children.sortedBy { it.lane.position }
|
||||
for (i in 0 until nChildren) {
|
||||
val c: GraphNode = 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
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.jetpackduba.gitnuro.git.graph
|
||||
|
||||
data class GraphCommitList2(
|
||||
val nodes: List<GraphNode2>,
|
||||
val maxLane: Int
|
||||
) : List<GraphNode2> by nodes
|
|
@ -1,5 +0,0 @@
|
|||
package com.jetpackduba.gitnuro.git.graph
|
||||
|
||||
const val INVALID_LANE_POSITION = -1
|
||||
|
||||
class GraphLane(var position: Int = 0)
|
|
@ -1,105 +0,0 @@
|
|||
package com.jetpackduba.gitnuro.git.graph
|
||||
|
||||
import org.eclipse.jgit.lib.AnyObjectId
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
|
||||
private val NO_CHILDREN = arrayOf<GraphNode>()
|
||||
private val NO_LANES = arrayOf<GraphLane>()
|
||||
private val NO_LANE = GraphLane(INVALID_LANE_POSITION)
|
||||
val NO_REFS = listOf<Ref>()
|
||||
|
||||
open class GraphNode(id: AnyObjectId?) : RevCommit(id), IGraphNode {
|
||||
var forkingOffLanes: Array<GraphLane> = NO_LANES
|
||||
var passingLanes: Array<GraphLane> = NO_LANES
|
||||
var mergingLanes: Array<GraphLane> = NO_LANES
|
||||
var lane: GraphLane = NO_LANE
|
||||
var children: Array<GraphNode> = NO_CHILDREN
|
||||
var refs: List<Ref> = NO_REFS
|
||||
var isStash: Boolean = false
|
||||
|
||||
fun addForkingOffLane(graphLane: GraphLane) {
|
||||
forkingOffLanes = addLane(graphLane, forkingOffLanes)
|
||||
}
|
||||
|
||||
fun addPassingLane(graphLane: GraphLane) {
|
||||
passingLanes = addLane(graphLane, passingLanes)
|
||||
}
|
||||
|
||||
fun addMergingLane(graphLane: GraphLane) {
|
||||
mergingLanes = addLane(graphLane, mergingLanes)
|
||||
}
|
||||
|
||||
fun addChild(c: GraphNode, addFirst: Boolean = false) {
|
||||
when (val childrenCount = children.count()) {
|
||||
0 -> children = arrayOf(c)
|
||||
1 -> {
|
||||
if (!c.id.equals(children[0].id)) {
|
||||
children = if (addFirst) {
|
||||
arrayOf(c, children[0])
|
||||
} else
|
||||
arrayOf(children[0], c)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
for (pc in children)
|
||||
if (c.id.equals(pc.id))
|
||||
return
|
||||
|
||||
val resultArray = if (addFirst) {
|
||||
val childList = mutableListOf(c)
|
||||
childList.addAll(children)
|
||||
childList.toTypedArray()
|
||||
} else {
|
||||
children.copyOf(childrenCount + 1).run {
|
||||
this[childrenCount] = c
|
||||
requireNoNulls()
|
||||
}
|
||||
}
|
||||
|
||||
children = resultArray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val childCount: Int
|
||||
get() {
|
||||
return children.size
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
forkingOffLanes = NO_LANES
|
||||
passingLanes = NO_LANES
|
||||
mergingLanes = NO_LANES
|
||||
children = NO_CHILDREN
|
||||
lane = NO_LANE
|
||||
super.reset()
|
||||
}
|
||||
|
||||
private fun addLane(graphLane: GraphLane, lanes: Array<GraphLane>): Array<GraphLane> {
|
||||
var newLines = lanes
|
||||
|
||||
when (val linesCount = newLines.count()) {
|
||||
0 -> newLines = arrayOf(graphLane)
|
||||
1 -> newLines = arrayOf(newLines[0], graphLane)
|
||||
else -> {
|
||||
val n = newLines.copyOf(linesCount + 1).run {
|
||||
this[linesCount] = graphLane
|
||||
requireNoNulls()
|
||||
}
|
||||
|
||||
newLines = n
|
||||
}
|
||||
}
|
||||
|
||||
return newLines
|
||||
}
|
||||
|
||||
override val graphParentCount: Int
|
||||
get() = parentCount
|
||||
|
||||
override fun getGraphParent(nth: Int): GraphNode {
|
||||
return getParent(nth) as GraphNode
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.jetpackduba.gitnuro.git.graph
|
||||
|
||||
import org.eclipse.jgit.lib.AnyObjectId
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
|
||||
|
||||
data class GraphNode2(
|
||||
val name: String,
|
||||
val message: String,
|
||||
val fullMessage: String,
|
||||
val authorIdent: PersonIdent,
|
||||
val committerIdent: PersonIdent,
|
||||
val parentCount: Int,
|
||||
val isStash: Boolean,
|
||||
val lane: Int,
|
||||
val passingLanes: List<Int>,
|
||||
val forkingLanes: List<Int>,
|
||||
val mergingLanes: List<Int>,
|
||||
val refs: List<Ref>,
|
||||
)
|
|
@ -1,172 +0,0 @@
|
|||
package com.jetpackduba.gitnuro.git.graph
|
||||
|
||||
import org.eclipse.jgit.errors.MissingObjectException
|
||||
import org.eclipse.jgit.internal.JGitText
|
||||
import org.eclipse.jgit.lib.AnyObjectId
|
||||
import org.eclipse.jgit.lib.Constants
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
import org.eclipse.jgit.lib.Repository
|
||||
import org.eclipse.jgit.revwalk.*
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Specialized RevWalk to be used for load data in a structured way to be displayed in a graph of commits.
|
||||
*/
|
||||
class GraphWalk(private var repository: Repository?) : RevWalk(repository) {
|
||||
private var additionalRefMap: MutableMap<AnyObjectId, Set<Ref>>? = HashMap()
|
||||
private var reverseRefMap: MutableMap<AnyObjectId, Set<Ref>>? = null
|
||||
|
||||
init {
|
||||
super.sort(RevSort.TOPO, true)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
super.dispose()
|
||||
if (reverseRefMap != null) {
|
||||
reverseRefMap?.clear()
|
||||
reverseRefMap = null
|
||||
}
|
||||
if (additionalRefMap != null) {
|
||||
additionalRefMap?.clear()
|
||||
additionalRefMap = null
|
||||
}
|
||||
repository = null
|
||||
}
|
||||
|
||||
override fun sort(revSort: RevSort, use: Boolean) {
|
||||
require(!(revSort == RevSort.TOPO && !use)) {
|
||||
JGitText.get().topologicalSortRequired
|
||||
}
|
||||
|
||||
super.sort(revSort, use)
|
||||
}
|
||||
|
||||
override fun createCommit(id: AnyObjectId): RevCommit {
|
||||
return GraphNode(id)
|
||||
}
|
||||
|
||||
override fun next(): RevCommit? {
|
||||
val graphNode = super.next() as GraphNode?
|
||||
|
||||
if (graphNode != null) {
|
||||
val refs = getRefs(graphNode)
|
||||
|
||||
graphNode.isStash = refs.count() == 1 && refs.firstOrNull()?.name == "refs/stash"
|
||||
graphNode.refs = refs
|
||||
}
|
||||
|
||||
return graphNode
|
||||
}
|
||||
|
||||
private fun getRefs(commitId: AnyObjectId): List<Ref> {
|
||||
val repository = this.repository
|
||||
var reverseRefMap = this.reverseRefMap
|
||||
var additionalRefMap = this.additionalRefMap
|
||||
if (reverseRefMap == null && repository != null && additionalRefMap != null) {
|
||||
|
||||
reverseRefMap = repository.allRefsByPeeledObjectId
|
||||
this.reverseRefMap = reverseRefMap
|
||||
|
||||
for (entry in additionalRefMap.entries) {
|
||||
val refsSet = reverseRefMap[entry.key]
|
||||
var additional = entry.value.toMutableSet()
|
||||
|
||||
if (refsSet != null) {
|
||||
if (additional.size == 1) {
|
||||
// It's an unmodifiable singleton set...
|
||||
additional = HashSet(additional)
|
||||
}
|
||||
additional.addAll(refsSet)
|
||||
}
|
||||
reverseRefMap[entry.key] = additional
|
||||
}
|
||||
|
||||
additionalRefMap.clear()
|
||||
additionalRefMap = null
|
||||
|
||||
this.additionalRefMap = additionalRefMap
|
||||
}
|
||||
|
||||
requireNotNull(reverseRefMap) // This should never be null
|
||||
|
||||
val refsSet = reverseRefMap[commitId]
|
||||
?: return NO_REFS
|
||||
val tags = refsSet.toList()
|
||||
|
||||
tags.sortedWith(GraphRefComparator())
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
fun markStartAllRefs(prefix: String) {
|
||||
repository?.let { repo ->
|
||||
for (ref in repo.refDatabase.getRefsByPrefix(prefix)) {
|
||||
if (ref.isSymbolic) continue
|
||||
markStartRef(ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markStartRef(ref: Ref) {
|
||||
try {
|
||||
val refTarget = parseAny(ref.leaf.objectId)
|
||||
|
||||
when (refTarget) {
|
||||
is RevCommit -> markStart(refTarget)
|
||||
// RevTag case handles commits without branches but only tags.
|
||||
is RevTag -> {
|
||||
if (refTarget.`object` is RevCommit) {
|
||||
val commit = lookupCommit(refTarget.`object`)
|
||||
markStart(commit)
|
||||
} else {
|
||||
println("Tag ${refTarget.tagName} is pointing to ${refTarget.`object`::class.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: MissingObjectException) {
|
||||
// Ignore missing Refs
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class GraphRefComparator : Comparator<Ref> {
|
||||
override fun compare(o1: Ref, o2: Ref): Int {
|
||||
try {
|
||||
val obj1 = parseAny(o1.objectId)
|
||||
val obj2 = parseAny(o2.objectId)
|
||||
val t1 = timeOf(obj1)
|
||||
val t2 = timeOf(obj2)
|
||||
if (t1 > t2) return -1
|
||||
if (t1 < t2) return 1
|
||||
} catch (e: IOException) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
var cmp = kind(o1) - kind(o2)
|
||||
|
||||
if (cmp == 0)
|
||||
cmp = o1.name.compareTo(o2.name)
|
||||
|
||||
return cmp
|
||||
}
|
||||
|
||||
private fun timeOf(revObject: RevObject): Long {
|
||||
if (revObject is RevCommit) return revObject.commitTime.toLong()
|
||||
if (revObject is RevTag) {
|
||||
try {
|
||||
parseBody(revObject)
|
||||
} catch (e: IOException) {
|
||||
return 0
|
||||
}
|
||||
val who = revObject.taggerIdent
|
||||
return who?.getWhen()?.time ?: 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun kind(r: Ref): Int {
|
||||
if (r.name.startsWith(Constants.R_TAGS)) return 0
|
||||
if (r.name.startsWith(Constants.R_HEADS)) return 1
|
||||
return if (r.name.startsWith(Constants.R_REMOTES)) 2 else 3
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package com.jetpackduba.gitnuro.git.graph
|
||||
|
||||
interface IGraphNode {
|
||||
val graphParentCount: Int
|
||||
fun getGraphParent(nth: Int): GraphNode
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package com.jetpackduba.gitnuro.git.graph
|
||||
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
|
||||
class UncommittedChangesGraphNode : GraphNode(ObjectId(0, 0, 0, 0, 0)) {
|
||||
|
||||
var graphParent: GraphNode? = null
|
||||
|
||||
override val graphParentCount: Int
|
||||
get() = 1 // Uncommitted changes can have a max of 1 parent commit
|
||||
|
||||
override fun getGraphParent(nth: Int): GraphNode {
|
||||
return requireNotNull(graphParent)
|
||||
}
|
||||
}
|
|
@ -1,66 +1,38 @@
|
|||
package com.jetpackduba.gitnuro.git.log
|
||||
|
||||
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphCommitList
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphWalk
|
||||
import com.jetpackduba.gitnuro.git.graph.GenerateLogWalkUseCase
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphCommitList2
|
||||
import com.jetpackduba.gitnuro.git.stash.GetStashListUseCase
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.Constants
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
import org.eclipse.jgit.lib.Repository
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetLogUseCase @Inject constructor() {
|
||||
private var graphWalkCached: GraphWalk? = null
|
||||
|
||||
suspend operator fun invoke(git: Git, currentBranch: Ref?, hasUncommittedChanges: Boolean, commitsLimit: Int) =
|
||||
class GetLogUseCase @Inject constructor(
|
||||
private val getStashListUseCase: GetStashListUseCase,
|
||||
private val generateLogWalkUseCase: GenerateLogWalkUseCase,
|
||||
) {
|
||||
suspend operator fun invoke(git: Git, hasUncommittedChanges: Boolean, commitsLimit: Int?) =
|
||||
withContext(Dispatchers.IO) {
|
||||
val commitList = GraphCommitList()
|
||||
val repositoryState = git.repository.repositoryState
|
||||
|
||||
if (currentBranch != null || repositoryState.isRebasing) { // Current branch is null when there is no log (new repo) or rebasing
|
||||
val logList = git.log().setMaxCount(1).call().toList()
|
||||
|
||||
val walk = GraphWalk(git.repository)
|
||||
|
||||
walk.use {
|
||||
// Without this, during rebase conflicts the graph won't show the HEAD commits (new commits created
|
||||
// by the rebase)
|
||||
walk.markStart(walk.lookupCommit(logList.first()))
|
||||
|
||||
walk.markStartAllRefs(Constants.R_HEADS)
|
||||
walk.markStartAllRefs(Constants.R_REMOTES)
|
||||
walk.markStartAllRefs(Constants.R_TAGS)
|
||||
walk.markStartAllRefs(Constants.R_STASH)
|
||||
|
||||
if (hasUncommittedChanges)
|
||||
commitList.addUncommittedChangesGraphCommit(logList.first())
|
||||
|
||||
commitList.source(walk)
|
||||
commitList.fillTo(commitsLimit)
|
||||
}
|
||||
|
||||
ensureActive()
|
||||
val logList = git.log().setMaxCount(1).call().toList()
|
||||
val firstCommit = logList.firstOrNull()
|
||||
val allRefs =
|
||||
git.repository.refDatabase.refs.filterNot { it.name.startsWith(Constants.R_STASH) } // remove stash as it only returns the latest, we get all afterward
|
||||
val stashes = getStashListUseCase(git)
|
||||
|
||||
return@withContext if (firstCommit == null) {
|
||||
GraphCommitList2(emptyList(), 0)
|
||||
} else {
|
||||
generateLogWalkUseCase.invoke(
|
||||
git,
|
||||
firstCommit,
|
||||
allRefs,
|
||||
stashes,
|
||||
hasUncommittedChanges,
|
||||
commitsLimit
|
||||
)
|
||||
}
|
||||
|
||||
commitList.calcMaxLine()
|
||||
|
||||
return@withContext commitList
|
||||
}
|
||||
|
||||
private fun cachedGraphWalk(repository: Repository): GraphWalk {
|
||||
val graphWalkCached = this.graphWalkCached
|
||||
|
||||
return if (graphWalkCached != null) {
|
||||
graphWalkCached
|
||||
} else {
|
||||
val newGraphWalk = GraphWalk(repository)
|
||||
this.graphWalkCached = newGraphWalk
|
||||
|
||||
newGraphWalk
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ class OpenFilePickerUseCase @Inject constructor(
|
|||
|
||||
if (isZenityInstalled) {
|
||||
val command = when (pickerType) {
|
||||
PickerType.FILES -> listOf(
|
||||
PickerType.FILES, PickerType.FILES_AND_DIRECTORIES -> listOf(
|
||||
"zenity",
|
||||
"--file-selection",
|
||||
"--title=Open"
|
||||
|
@ -70,21 +70,15 @@ class OpenFilePickerUseCase @Inject constructor(
|
|||
}
|
||||
|
||||
if (isMac) {
|
||||
if (pickerType == PickerType.DIRECTORIES) {
|
||||
System.setProperty("apple.awt.fileDialogForDirectories", "true")
|
||||
}
|
||||
|
||||
val fileChooser = if (basePath.isNullOrEmpty()) {
|
||||
System.setProperty("apple.awt.fileDialogForDirectories", "true")
|
||||
val fileChooser = if (basePath.isNullOrEmpty())
|
||||
FileDialog(null as java.awt.Frame?, "Open", FileDialog.LOAD)
|
||||
} else {
|
||||
else
|
||||
FileDialog(null as java.awt.Frame?, "Open", FileDialog.LOAD).apply {
|
||||
directory = basePath
|
||||
}
|
||||
}
|
||||
|
||||
fileChooser.isMultipleMode = false
|
||||
fileChooser.isVisible = true
|
||||
|
||||
System.setProperty("apple.awt.fileDialogForDirectories", "false")
|
||||
|
||||
if (fileChooser.file != null && fileChooser.directory != null) {
|
||||
|
@ -109,5 +103,6 @@ class OpenFilePickerUseCase @Inject constructor(
|
|||
|
||||
enum class PickerType(val value: Int) {
|
||||
FILES(JFileChooser.FILES_ONLY),
|
||||
DIRECTORIES(JFileChooser.DIRECTORIES_ONLY);
|
||||
DIRECTORIES(JFileChooser.DIRECTORIES_ONLY),
|
||||
FILES_AND_DIRECTORIES(JFileChooser.FILES_AND_DIRECTORIES);
|
||||
}
|
|
@ -453,6 +453,7 @@ private fun CommitTreeItemEntry(
|
|||
|
||||
is TreeItem.Dir -> DirectoryEntry(
|
||||
dirName = entry.displayName,
|
||||
isExpanded = entry.isExpanded,
|
||||
onClick = { onDirectoryClick(entry) },
|
||||
depth = entry.depth,
|
||||
onGenerateContextMenu = { onGenerateDirectoryContextMenu(entry) },
|
||||
|
|
|
@ -1124,58 +1124,15 @@ private fun UncommittedTreeItemEntry(
|
|||
)
|
||||
|
||||
is TreeItem.Dir -> DirectoryEntry(
|
||||
entry.displayName,
|
||||
onClick,
|
||||
dirName = entry.displayName,
|
||||
isExpanded = entry.isExpanded,
|
||||
onClick = onClick,
|
||||
depth = entry.depth,
|
||||
onGenerateContextMenu = { onGenerateDirectoryContextMenu(entry) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//@Composable
|
||||
//private fun TreeItemEntry(
|
||||
// entry: TreeItem<StatusEntry>,
|
||||
// isSelected: Boolean,
|
||||
// actionTitle: String,
|
||||
// actionColor: Color,
|
||||
// onClick: () -> Unit,
|
||||
// onButtonClick: () -> Unit,
|
||||
// onGenerateContextMenu: (StatusEntry) -> List<ContextMenuElement>,
|
||||
// onGenerateDirectoryContextMenu: (TreeItem.Dir) -> List<ContextMenuElement>,
|
||||
//) {
|
||||
// when (entry) {
|
||||
// is TreeItem.File -> TreeFileEntry(
|
||||
// entry,
|
||||
// isSelected,
|
||||
// actionTitle,
|
||||
// actionColor,
|
||||
// onClick,
|
||||
// onButtonClick,
|
||||
// onGenerateContextMenu,
|
||||
// )
|
||||
//
|
||||
// is TreeItem.Dir -> TreeDirEntry(
|
||||
// entry,
|
||||
// onClick,
|
||||
// onGenerateDirectoryContextMenu,
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
|
||||
internal fun placeRightOrBottom(
|
||||
totalSize: Int,
|
||||
size: IntArray,
|
||||
outPosition: IntArray,
|
||||
reverseInput: Boolean
|
||||
) {
|
||||
val consumedSize = size.fold(0) { a, b -> a + b }
|
||||
var current = totalSize - consumedSize
|
||||
size.forEachIndexed(reverseInput) { index, it ->
|
||||
outPosition[index] = current
|
||||
current += it
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun IntArray.forEachIndexed(reversed: Boolean, action: (Int, Int) -> Unit) {
|
||||
if (!reversed) {
|
||||
forEachIndexed(action)
|
||||
|
|
|
@ -141,13 +141,14 @@ fun FileEntry(
|
|||
@Composable
|
||||
fun DirectoryEntry(
|
||||
dirName: String,
|
||||
isExpanded: Boolean,
|
||||
onClick: () -> Unit,
|
||||
depth: Int = 0,
|
||||
onGenerateContextMenu: () -> List<ContextMenuElement>,
|
||||
) {
|
||||
|
||||
FileEntry(
|
||||
icon = painterResource(AppIcons.FOLDER),
|
||||
icon = painterResource(if (isExpanded) AppIcons.FOLDER_OPEN else AppIcons.FOLDER),
|
||||
iconColor = MaterialTheme.colors.onBackground,
|
||||
isSelected = false,
|
||||
onClick = onClick,
|
||||
|
|
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.*
|
|||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
|
@ -30,8 +29,6 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.input.pointer.onPointerEvent
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
@ -41,17 +38,14 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.jetpackduba.gitnuro.AppIcons
|
||||
import com.jetpackduba.gitnuro.extensions.*
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphCommitList
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphNode
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphCommitList2
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphNode2
|
||||
import com.jetpackduba.gitnuro.git.workspace.StatusSummary
|
||||
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
|
||||
import com.jetpackduba.gitnuro.keybindings.matchesBinding
|
||||
import com.jetpackduba.gitnuro.theme.*
|
||||
import com.jetpackduba.gitnuro.ui.SelectedItem
|
||||
import com.jetpackduba.gitnuro.ui.components.AvatarImage
|
||||
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
|
||||
import com.jetpackduba.gitnuro.ui.components.gitnuroDynamicViewModel
|
||||
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
|
||||
import com.jetpackduba.gitnuro.ui.components.*
|
||||
import com.jetpackduba.gitnuro.ui.components.tooltip.InstantTooltip
|
||||
import com.jetpackduba.gitnuro.ui.components.tooltip.InstantTooltipPosition
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.*
|
||||
|
@ -81,8 +75,6 @@ private const val CANVAS_MIN_WIDTH = 100
|
|||
private const val CANVAS_DEFAULT_WIDTH = 120
|
||||
private const val MIN_GRAPH_LANES = 2
|
||||
|
||||
private const val HORIZONTAL_SCROLL_PIXELS_MULTIPLIER = 10
|
||||
|
||||
/**
|
||||
* Additional number of lanes to simulate to create a margin at the end of the graph.
|
||||
*/
|
||||
|
@ -183,7 +175,7 @@ private fun LogLoaded(
|
|||
if (graphWidth.value < CANVAS_MIN_WIDTH) graphWidth = CANVAS_MIN_WIDTH.dp
|
||||
|
||||
val maxLinePosition = if (commitList.isNotEmpty())
|
||||
commitList.maxLine
|
||||
commitList.maxLane
|
||||
else
|
||||
MIN_GRAPH_LANES
|
||||
|
||||
|
@ -315,7 +307,7 @@ private fun LogLoaded(
|
|||
|
||||
suspend fun scrollToCommit(
|
||||
verticalScrollState: LazyListState,
|
||||
commitList: GraphCommitList,
|
||||
commitList: GraphCommitList2,
|
||||
commit: RevCommit?,
|
||||
) {
|
||||
val index = commitList.indexOfFirst { it.name == commit?.name }
|
||||
|
@ -327,7 +319,7 @@ suspend fun scrollToCommit(
|
|||
|
||||
suspend fun scrollToUncommittedChanges(
|
||||
verticalScrollState: LazyListState,
|
||||
commitList: GraphCommitList,
|
||||
commitList: GraphCommitList2,
|
||||
) {
|
||||
if (commitList.isNotEmpty())
|
||||
verticalScrollState.scrollToItem(0)
|
||||
|
@ -433,17 +425,16 @@ fun SearchFilter(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun CommitsList(
|
||||
scrollState: LazyListState,
|
||||
hasUncommittedChanges: Boolean,
|
||||
searchFilter: List<GraphNode>?,
|
||||
searchFilter: List<GraphNode2>?,
|
||||
selectedCommit: RevCommit?,
|
||||
logStatus: LogStatus.Loaded,
|
||||
repositoryState: RepositoryState,
|
||||
selectedItem: SelectedItem,
|
||||
commitList: GraphCommitList,
|
||||
commitList: GraphCommitList2,
|
||||
logViewModel: LogViewModel,
|
||||
commitsLimit: Int,
|
||||
onMerge: (Ref) -> Unit,
|
||||
|
@ -452,22 +443,9 @@ fun CommitsList(
|
|||
graphWidth: Dp,
|
||||
horizontalScrollState: ScrollState,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
ScrollableLazyColumn(
|
||||
state = scrollState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
// The underlying composable assigned to the horizontal scroll bar won't be receiving the scroll events
|
||||
// because the commits list will consume the events, so this code tries to scroll manually when it detects
|
||||
// horizontal scrolling
|
||||
.onPointerEvent(PointerEventType.Scroll) { pointerEvent ->
|
||||
scope.launch {
|
||||
val xScroll = pointerEvent.changes.map { it.scrollDelta.x }.sum()
|
||||
horizontalScrollState.scrollBy(xScroll * HORIZONTAL_SCROLL_PIXELS_MULTIPLIER)
|
||||
}
|
||||
println(pointerEvent)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
if (
|
||||
hasUncommittedChanges ||
|
||||
|
@ -507,19 +485,19 @@ fun CommitsList(
|
|||
graphNode = graphNode,
|
||||
isSelected = selectedCommit?.name == graphNode.name,
|
||||
currentBranch = logStatus.currentBranch,
|
||||
matchesSearchFilter = searchFilter?.contains(graphNode),
|
||||
matchesSearchFilter = false, //searchFilter?.contains(graphNode),
|
||||
horizontalScrollState = horizontalScrollState,
|
||||
showCreateNewBranch = { onShowLogDialog(LogDialog.NewBranch(graphNode)) },
|
||||
showCreateNewTag = { onShowLogDialog(LogDialog.NewTag(graphNode)) },
|
||||
resetBranch = { onShowLogDialog(LogDialog.ResetBranch(graphNode)) },
|
||||
onMergeBranch = onMerge,
|
||||
onRebaseBranch = onRebase,
|
||||
onRebaseInteractive = { logViewModel.rebaseInteractive(graphNode) },
|
||||
onRebaseInteractive = { /*logViewModel.rebaseInteractive(graphNode)*/ },
|
||||
onRevCommitSelected = { logViewModel.selectLogLine(graphNode) },
|
||||
onChangeDefaultUpstreamBranch = { onShowLogDialog(LogDialog.ChangeDefaultBranch(it)) },
|
||||
onDeleteStash = { logViewModel.deleteStash(graphNode) },
|
||||
onApplyStash = { logViewModel.applyStash(graphNode) },
|
||||
onPopStash = { logViewModel.popStash(graphNode) },
|
||||
onDeleteStash = { /*logViewModel.deleteStash(graphNode)*/ },
|
||||
onApplyStash = { /*logViewModel.applyStash(graphNode)*/ },
|
||||
onPopStash = { /*logViewModel.popStash(graphNode)*/ },
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -754,7 +732,7 @@ fun SummaryEntry(
|
|||
private fun CommitLine(
|
||||
graphWidth: Dp,
|
||||
logViewModel: LogViewModel,
|
||||
graphNode: GraphNode,
|
||||
graphNode: GraphNode2,
|
||||
isSelected: Boolean,
|
||||
currentBranch: Ref?,
|
||||
matchesSearchFilter: Boolean?,
|
||||
|
@ -771,7 +749,7 @@ private fun CommitLine(
|
|||
onChangeDefaultUpstreamBranch: (Ref) -> Unit,
|
||||
horizontalScrollState: ScrollState,
|
||||
) {
|
||||
val isLastCommitOfCurrentBranch = currentBranch?.objectId?.name == graphNode.id.name
|
||||
val isLastCommitOfCurrentBranch = currentBranch?.objectId?.name == graphNode.name
|
||||
|
||||
ContextMenu(
|
||||
items = {
|
||||
|
@ -799,7 +777,7 @@ private fun CommitLine(
|
|||
modifier = Modifier
|
||||
.clickable { onRevCommitSelected() }
|
||||
) {
|
||||
val nodeColor = colors[graphNode.lane.position % colors.size]
|
||||
val nodeColor = colors[graphNode.lane/*.position*/ % colors.size]
|
||||
|
||||
Box {
|
||||
Row(
|
||||
|
@ -865,7 +843,7 @@ private fun CommitLine(
|
|||
|
||||
@Composable
|
||||
fun CommitMessage(
|
||||
commit: GraphNode,
|
||||
commit: GraphNode2,
|
||||
currentBranch: Ref?,
|
||||
nodeColor: Color,
|
||||
matchesSearchFilter: Boolean?,
|
||||
|
@ -925,9 +903,9 @@ fun CommitMessage(
|
|||
}
|
||||
}
|
||||
|
||||
val message = remember(commit.id.name) {
|
||||
val message = commit.message /*remember(commit.id.name) {
|
||||
commit.getShortMessageTrimmed()
|
||||
}
|
||||
}*/
|
||||
|
||||
Text(
|
||||
text = message,
|
||||
|
@ -979,12 +957,12 @@ fun SimpleDividerLog(modifier: Modifier) {
|
|||
@Composable
|
||||
fun CommitsGraph(
|
||||
modifier: Modifier = Modifier,
|
||||
plotCommit: GraphNode,
|
||||
plotCommit: GraphNode2,
|
||||
nodeColor: Color,
|
||||
isSelected: Boolean,
|
||||
) {
|
||||
val passingLanes = plotCommit.passingLanes
|
||||
val forkingOffLanes = plotCommit.forkingOffLanes
|
||||
val forkingOffLanes = plotCommit.forkingLanes
|
||||
val mergingLanes = plotCommit.mergingLanes
|
||||
val density = LocalDensity.current.density
|
||||
val laneWidthWithDensity = remember(density) {
|
||||
|
@ -998,34 +976,34 @@ fun CommitsGraph(
|
|||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
|
||||
val itemPosition = plotCommit.lane.position
|
||||
val itemPosition = plotCommit.lane
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
clipRect {
|
||||
if (plotCommit.childCount > 0) {
|
||||
drawLine(
|
||||
color = colors[itemPosition % colors.size],
|
||||
start = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y),
|
||||
end = Offset(laneWidthWithDensity * (itemPosition + 1), 0f),
|
||||
strokeWidth = 2f,
|
||||
)
|
||||
}
|
||||
// if (plotCommit.childCount > 0) {
|
||||
// drawLine(
|
||||
// color = colors[itemPosition % colors.size],
|
||||
// start = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y),
|
||||
// end = Offset(laneWidthWithDensity * (itemPosition + 1), 0f),
|
||||
// strokeWidth = 2f,
|
||||
// )
|
||||
// }
|
||||
|
||||
forkingOffLanes.forEach { plotLane ->
|
||||
drawLine(
|
||||
color = colors[plotLane.position % colors.size],
|
||||
color = colors[plotLane % colors.size],
|
||||
start = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y),
|
||||
end = Offset(laneWidthWithDensity * (plotLane.position + 1), 0f),
|
||||
end = Offset(laneWidthWithDensity * (plotLane + 1), 0f),
|
||||
strokeWidth = 2f,
|
||||
)
|
||||
}
|
||||
|
||||
mergingLanes.forEach { plotLane ->
|
||||
drawLine(
|
||||
color = colors[plotLane.position % colors.size],
|
||||
start = Offset(laneWidthWithDensity * (plotLane.position + 1), this.size.height),
|
||||
color = colors[plotLane % colors.size],
|
||||
start = Offset(laneWidthWithDensity * (plotLane + 1), this.size.height),
|
||||
end = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y),
|
||||
strokeWidth = 2f,
|
||||
)
|
||||
|
@ -1042,9 +1020,9 @@ fun CommitsGraph(
|
|||
|
||||
passingLanes.forEach { plotLane ->
|
||||
drawLine(
|
||||
color = colors[plotLane.position % colors.size],
|
||||
start = Offset(laneWidthWithDensity * (plotLane.position + 1), 0f),
|
||||
end = Offset(laneWidthWithDensity * (plotLane.position + 1), this.size.height),
|
||||
color = colors[plotLane % colors.size],
|
||||
start = Offset(laneWidthWithDensity * (plotLane + 1), 0f),
|
||||
end = Offset(laneWidthWithDensity * (plotLane + 1), this.size.height),
|
||||
strokeWidth = 2f,
|
||||
)
|
||||
}
|
||||
|
@ -1064,7 +1042,7 @@ fun CommitsGraph(
|
|||
@Composable
|
||||
fun CommitNode(
|
||||
modifier: Modifier = Modifier,
|
||||
plotCommit: GraphNode,
|
||||
plotCommit: GraphNode2,
|
||||
color: Color,
|
||||
) {
|
||||
val author = plotCommit.authorIdent
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package com.jetpackduba.gitnuro.ui.log
|
||||
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphNode
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphNode2
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
|
||||
sealed interface LogDialog {
|
||||
data object None : LogDialog
|
||||
data class NewBranch(val graphNode: GraphNode) : LogDialog
|
||||
data class NewTag(val graphNode: GraphNode) : LogDialog
|
||||
data class ResetBranch(val graphNode: GraphNode) : LogDialog
|
||||
data class NewBranch(val graphNode: GraphNode2) : LogDialog
|
||||
data class NewTag(val graphNode: GraphNode2) : LogDialog
|
||||
data class ResetBranch(val graphNode: GraphNode2) : LogDialog
|
||||
data class ChangeDefaultBranch(val ref: Ref) : LogDialog
|
||||
}
|
|
@ -3,14 +3,13 @@ package com.jetpackduba.gitnuro.viewmodels
|
|||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import com.jetpackduba.gitnuro.extensions.delayedStateChange
|
||||
import com.jetpackduba.gitnuro.extensions.shortName
|
||||
import com.jetpackduba.gitnuro.extensions.simpleName
|
||||
import com.jetpackduba.gitnuro.git.RefreshType
|
||||
import com.jetpackduba.gitnuro.git.TabState
|
||||
import com.jetpackduba.gitnuro.git.TaskEvent
|
||||
import com.jetpackduba.gitnuro.git.branches.*
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphCommitList
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphNode
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphCommitList2
|
||||
import com.jetpackduba.gitnuro.git.graph.GraphNode2
|
||||
import com.jetpackduba.gitnuro.git.log.*
|
||||
import com.jetpackduba.gitnuro.git.rebase.RebaseBranchUseCase
|
||||
import com.jetpackduba.gitnuro.git.rebase.StartRebaseInteractiveUseCase
|
||||
|
@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.*
|
|||
import kotlinx.coroutines.launch
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.api.errors.CheckoutConflictException
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
import javax.inject.Inject
|
||||
|
@ -147,14 +147,14 @@ class LogViewModel @Inject constructor(
|
|||
val commitsLimit = if (appSettings.commitsLimitEnabled) {
|
||||
appSettings.commitsLimit
|
||||
} else
|
||||
Int.MAX_VALUE
|
||||
null
|
||||
|
||||
val commitsLimitDisplayed = if (appSettings.commitsLimitEnabled) {
|
||||
appSettings.commitsLimit
|
||||
} else
|
||||
-1
|
||||
|
||||
val log = getLogUseCase(git, currentBranch, hasUncommittedChanges, commitsLimit)
|
||||
val log = getLogUseCase(git, hasUncommittedChanges, commitsLimit)
|
||||
|
||||
_logStatus.value =
|
||||
LogStatus.Loaded(hasUncommittedChanges, log, currentBranch, statusSummary, commitsLimitDisplayed)
|
||||
|
@ -189,29 +189,29 @@ class LogViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
fun checkoutCommit(revCommit: RevCommit) = tabState.safeProcessing(
|
||||
fun checkoutCommit(revCommit: GraphNode2) = tabState.safeProcessing(
|
||||
refreshType = RefreshType.ALL_DATA,
|
||||
title = "Commit checkout",
|
||||
subtitle = "Checking out commit ${revCommit.name}",
|
||||
) { git ->
|
||||
checkoutCommitUseCase(git, revCommit)
|
||||
// checkoutCommitUseCase(git, revCommit)
|
||||
}
|
||||
|
||||
fun revertCommit(revCommit: RevCommit) = tabState.safeProcessing(
|
||||
fun revertCommit(revCommit: GraphNode2) = tabState.safeProcessing(
|
||||
refreshType = RefreshType.ALL_DATA,
|
||||
title = "Commit revert",
|
||||
subtitle = "Reverting commit ${revCommit.name}",
|
||||
refreshEvenIfCrashes = true,
|
||||
) { git ->
|
||||
revertCommitUseCase(git, revCommit)
|
||||
// revertCommitUseCase(git, revCommit)
|
||||
}
|
||||
|
||||
fun resetToCommit(revCommit: RevCommit, resetType: ResetType) = tabState.safeProcessing(
|
||||
fun resetToCommit(revCommit: GraphNode2, resetType: ResetType) = tabState.safeProcessing(
|
||||
refreshType = RefreshType.ALL_DATA,
|
||||
title = "Branch reset",
|
||||
subtitle = "Reseting branch to commit ${revCommit.shortName}",
|
||||
// subtitle = "Reseting branch to commit ${revCommit.shortName}",
|
||||
) { git ->
|
||||
resetToCommitUseCase(git, revCommit, resetType = resetType)
|
||||
// resetToCommitUseCase(git, revCommit, resetType = resetType)
|
||||
}
|
||||
|
||||
fun checkoutRef(ref: Ref) = tabState.safeProcessing(
|
||||
|
@ -222,29 +222,29 @@ class LogViewModel @Inject constructor(
|
|||
checkoutRefUseCase(git, ref)
|
||||
}
|
||||
|
||||
fun cherrypickCommit(revCommit: RevCommit) = tabState.safeProcessing(
|
||||
fun cherrypickCommit(revCommit: GraphNode2) = tabState.safeProcessing(
|
||||
refreshType = RefreshType.UNCOMMITTED_CHANGES_AND_LOG,
|
||||
title = "Cherry-pick",
|
||||
subtitle = "Cherry-picking commit ${revCommit.shortName}",
|
||||
// subtitle = "Cherry-picking commit ${revCommit.shortName}",
|
||||
) { git ->
|
||||
cherryPickCommitUseCase(git, revCommit)
|
||||
// cherryPickCommitUseCase(git, revCommit)
|
||||
}
|
||||
|
||||
fun createBranchOnCommit(branch: String, revCommit: RevCommit) = tabState.safeProcessing(
|
||||
fun createBranchOnCommit(branch: String, revCommit: GraphNode2) = tabState.safeProcessing(
|
||||
refreshType = RefreshType.ALL_DATA,
|
||||
title = "New branch",
|
||||
subtitle = "Creating new branch \"$branch\" on commit ${revCommit.shortName}",
|
||||
// subtitle = "Creating new branch \"$branch\" on commit ${revCommit.shortName}",
|
||||
refreshEvenIfCrashesInteractive = { it is CheckoutConflictException },
|
||||
) { git ->
|
||||
createBranchOnCommitUseCase(git, branch, revCommit)
|
||||
// createBranchOnCommitUseCase(git, branch, revCommit)
|
||||
}
|
||||
|
||||
fun createTagOnCommit(tag: String, revCommit: RevCommit) = tabState.safeProcessing(
|
||||
fun createTagOnCommit(tag: String, revCommit: GraphNode2) = tabState.safeProcessing(
|
||||
refreshType = RefreshType.ALL_DATA,
|
||||
title = "New tag",
|
||||
subtitle = "Creating new tag \"$tag\" on commit ${revCommit.shortName}",
|
||||
// subtitle = "Creating new tag \"$tag\" on commit ${revCommit.shortName}",
|
||||
) { git ->
|
||||
createTagOnCommitUseCase(git, tag, revCommit)
|
||||
// createTagOnCommitUseCase(git, tag, revCommit)
|
||||
}
|
||||
|
||||
fun mergeBranch(ref: Ref) = tabState.safeProcessing(
|
||||
|
@ -331,10 +331,12 @@ class LogViewModel @Inject constructor(
|
|||
NONE_MATCHING_INDEX
|
||||
}
|
||||
|
||||
fun selectLogLine(commit: GraphNode) = tabState.runOperation(
|
||||
fun selectLogLine(commit: GraphNode2) = tabState.runOperation(
|
||||
refreshType = RefreshType.NONE,
|
||||
) {
|
||||
tabState.newSelectedCommit(commit)
|
||||
) { git ->
|
||||
val oid = ObjectId.fromString(commit.name)
|
||||
tabState.newSelectedCommit(git.repository.parseCommit(oid))
|
||||
println("Commit SHA: ${commit.name}")
|
||||
|
||||
val searchValue = _logSearchFilterResults.value
|
||||
if (searchValue is LogSearch.SearchResults) {
|
||||
|
@ -371,7 +373,7 @@ class LogViewModel @Inject constructor(
|
|||
var startingUiIndex = NONE_MATCHING_INDEX
|
||||
|
||||
if (matchingCommits.isNotEmpty()) {
|
||||
_focusCommit.emit(matchingCommits.first())
|
||||
// TODO _focusCommit.emit(matchingCommits.first())
|
||||
startingUiIndex = FIRST_INDEX
|
||||
}
|
||||
|
||||
|
@ -397,7 +399,7 @@ class LogViewModel @Inject constructor(
|
|||
val newCommitToSelect = commits[newIndex - 1]
|
||||
|
||||
_logSearchFilterResults.value = logSearchFilterResultsValue.copy(index = newIndex)
|
||||
_focusCommit.emit(newCommitToSelect)
|
||||
//TODO _focusCommit.emit(newCommitToSelect)
|
||||
}
|
||||
|
||||
suspend fun selectNextFilterCommit() {
|
||||
|
@ -419,7 +421,7 @@ class LogViewModel @Inject constructor(
|
|||
val newCommitToSelect = commits[index]
|
||||
|
||||
_logSearchFilterResults.value = logSearchFilterResultsValue.copy(index = newIndex)
|
||||
_focusCommit.emit(newCommitToSelect)
|
||||
// TODO _focusCommit.emit(newCommitToSelect)
|
||||
}
|
||||
|
||||
fun showDialog(dialog: LogDialog) {
|
||||
|
@ -449,7 +451,7 @@ sealed interface LogStatus {
|
|||
data object Loading : LogStatus
|
||||
class Loaded(
|
||||
val hasUncommittedChanges: Boolean,
|
||||
val plotCommitList: GraphCommitList,
|
||||
val plotCommitList: GraphCommitList2,
|
||||
val currentBranch: Ref?,
|
||||
val statusSummary: StatusSummary,
|
||||
val commitsLimit: Int,
|
||||
|
@ -459,7 +461,7 @@ sealed interface LogStatus {
|
|||
sealed interface LogSearch {
|
||||
data object NotSearching : LogSearch
|
||||
data class SearchResults(
|
||||
val commits: List<GraphNode>,
|
||||
val commits: List<GraphNode2>,
|
||||
val index: Int,
|
||||
val totalCount: Int = commits.count(),
|
||||
) : LogSearch
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
package com.jetpackduba.gitnuro.git.graph
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
|
||||
class GenerateLogWalkUseCaseTest {
|
||||
|
||||
@Test
|
||||
fun getReservedLane() {
|
||||
val generateLogWalkUseCase = GenerateLogWalkUseCase()
|
||||
val reservedLanes = mapOf(
|
||||
0 to "*",
|
||||
1 to "A",
|
||||
2 to "B",
|
||||
3 to "C",
|
||||
4 to "D",
|
||||
5 to "E",
|
||||
6 to "F",
|
||||
)
|
||||
val reservedLane = generateLogWalkUseCase.getReservedLanes(reservedLanes, "A")
|
||||
assertEquals(listOf(1), reservedLane)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getReservedLane_when_value_not_present() {
|
||||
val generateLogWalkUseCase = GenerateLogWalkUseCase()
|
||||
val reservedLanes = mapOf(
|
||||
// 0 to "*",
|
||||
1 to "A",
|
||||
2 to "B",
|
||||
3 to "C",
|
||||
4 to "D",
|
||||
5 to "E",
|
||||
6 to "F",
|
||||
)
|
||||
val reservedLane = generateLogWalkUseCase.getReservedLanes(reservedLanes, "P")
|
||||
assertEquals(listOf(0), reservedLane)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun firstAvailableLane_without_first_item() {
|
||||
val generateLogWalkUseCase = GenerateLogWalkUseCase()
|
||||
val reservedLanes = mapOf(
|
||||
// 0 to "*",
|
||||
1 to "A",
|
||||
2 to "B",
|
||||
3 to "C",
|
||||
// 4 to "D",
|
||||
5 to "E",
|
||||
6 to "F",
|
||||
)
|
||||
|
||||
val firstAvailableLane = generateLogWalkUseCase.firstAvailableLane(reservedLanes)
|
||||
assertEquals(0, firstAvailableLane)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun firstAvailableLane_without_middle_item() {
|
||||
val generateLogWalkUseCase = GenerateLogWalkUseCase()
|
||||
val reservedLanes = mapOf(
|
||||
0 to "*",
|
||||
1 to "A",
|
||||
2 to "B",
|
||||
3 to "C",
|
||||
// 4 to "D",
|
||||
5 to "E",
|
||||
6 to "F",
|
||||
)
|
||||
|
||||
val firstAvailableLane = generateLogWalkUseCase.firstAvailableLane(reservedLanes)
|
||||
assertEquals(4, firstAvailableLane)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun firstAvailableLane_with_empty_reserved_lanes() {
|
||||
val generateLogWalkUseCase = GenerateLogWalkUseCase()
|
||||
val reservedLanes = mapOf<Int, String>()
|
||||
val firstAvailableLane = generateLogWalkUseCase.firstAvailableLane(reservedLanes)
|
||||
assertEquals(0, firstAvailableLane)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun firstAvailableLane_without_single_non_zero() {
|
||||
val generateLogWalkUseCase = GenerateLogWalkUseCase()
|
||||
val reservedLanes = mapOf(
|
||||
1 to "A",
|
||||
)
|
||||
|
||||
val firstAvailableLane = generateLogWalkUseCase.firstAvailableLane(reservedLanes)
|
||||
assertEquals(0, firstAvailableLane)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun firstAvailableLane_without_2_keys_non_zero() {
|
||||
val generateLogWalkUseCase = GenerateLogWalkUseCase()
|
||||
val reservedLanes = mapOf(
|
||||
1 to "A",
|
||||
2 to "B",
|
||||
)
|
||||
|
||||
val firstAvailableLane = generateLogWalkUseCase.firstAvailableLane(reservedLanes)
|
||||
assertEquals(0, firstAvailableLane)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue