Added new log tree
This commit is contained in:
parent
5113ca9a71
commit
87d7f1cdae
@ -15,7 +15,7 @@ import androidx.compose.ui.zIndex
|
|||||||
import app.di.DaggerAppComponent
|
import app.di.DaggerAppComponent
|
||||||
import app.git.GitManager
|
import app.git.GitManager
|
||||||
import app.git.RepositorySelectionStatus
|
import app.git.RepositorySelectionStatus
|
||||||
import app.theme.GitnuroTheme
|
import app.theme.AppTheme
|
||||||
import app.ui.RepositoryOpenPage
|
import app.ui.RepositoryOpenPage
|
||||||
import app.ui.WelcomePage
|
import app.ui.WelcomePage
|
||||||
import app.ui.components.RepositoriesTabPanel
|
import app.ui.components.RepositoriesTabPanel
|
||||||
@ -47,7 +47,7 @@ class Main {
|
|||||||
size = WindowSize(1280.dp, 720.dp)
|
size = WindowSize(1280.dp, 720.dp)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
GitnuroTheme {
|
AppTheme {
|
||||||
val tabs = remember {
|
val tabs = remember {
|
||||||
val tabName = mutableStateOf("New tab")
|
val tabName = mutableStateOf("New tab")
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
@ -61,7 +61,10 @@ class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var selectedTabKey by remember { mutableStateOf(0) }
|
var selectedTabKey by remember { mutableStateOf(0) }
|
||||||
Column {
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier.background(MaterialTheme.colors.surface)
|
||||||
|
) {
|
||||||
RepositoriesTabPanel(
|
RepositoriesTabPanel(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 4.dp, bottom = 2.dp, start = 4.dp, end = 4.dp)
|
.padding(top = 4.dp, bottom = 2.dp, start = 4.dp, end = 4.dp)
|
||||||
|
@ -4,5 +4,8 @@ import org.eclipse.jgit.lib.Ref
|
|||||||
|
|
||||||
val Ref.simpleName: String
|
val Ref.simpleName: String
|
||||||
get() {
|
get() {
|
||||||
return this.name.split("/").last()
|
return if (this.name.startsWith("refs/remotes/"))
|
||||||
|
name.replace("refs/remotes/", "")
|
||||||
|
else
|
||||||
|
this.name.split("/").last()
|
||||||
}
|
}
|
@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.eclipse.jgit.api.ListBranchCommand
|
||||||
import org.eclipse.jgit.lib.Ref
|
import org.eclipse.jgit.lib.Ref
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -18,9 +19,7 @@ class BranchesManager @Inject constructor() {
|
|||||||
get() = _currentBranch
|
get() = _currentBranch
|
||||||
|
|
||||||
suspend fun loadBranches(git: Git) = withContext(Dispatchers.IO) {
|
suspend fun loadBranches(git: Git) = withContext(Dispatchers.IO) {
|
||||||
val branchList = git
|
val branchList = getBranches(git)
|
||||||
.branchList()
|
|
||||||
.call()
|
|
||||||
|
|
||||||
val branchName = git
|
val branchName = git
|
||||||
.repository
|
.repository
|
||||||
@ -30,6 +29,13 @@ class BranchesManager @Inject constructor() {
|
|||||||
_currentBranch.value = branchName
|
_currentBranch.value = branchName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getBranches(git: Git) = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext git
|
||||||
|
.branchList()
|
||||||
|
.setListMode(ListBranchCommand.ListMode.ALL)
|
||||||
|
.call()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun createBranch(git: Git, branchName: String) = withContext(Dispatchers.IO) {
|
suspend fun createBranch(git: Git, branchName: String) = withContext(Dispatchers.IO) {
|
||||||
git
|
git
|
||||||
.branchCreate()
|
.branchCreate()
|
||||||
|
@ -5,8 +5,10 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.diff.DiffEntry
|
import org.eclipse.jgit.diff.DiffEntry
|
||||||
import org.eclipse.jgit.diff.DiffFormatter
|
import org.eclipse.jgit.diff.DiffFormatter
|
||||||
|
import org.eclipse.jgit.diff.EditList
|
||||||
import org.eclipse.jgit.dircache.DirCacheIterator
|
import org.eclipse.jgit.dircache.DirCacheIterator
|
||||||
import org.eclipse.jgit.lib.Repository
|
import org.eclipse.jgit.lib.Repository
|
||||||
|
import org.eclipse.jgit.patch.HunkHeader
|
||||||
import org.eclipse.jgit.revplot.PlotCommit
|
import org.eclipse.jgit.revplot.PlotCommit
|
||||||
import org.eclipse.jgit.revplot.PlotCommitList
|
import org.eclipse.jgit.revplot.PlotCommitList
|
||||||
import org.eclipse.jgit.revwalk.RevCommit
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
@ -25,6 +27,7 @@ class DiffManager @Inject constructor() {
|
|||||||
|
|
||||||
DiffFormatter(byteArrayOutputStream).use { formatter ->
|
DiffFormatter(byteArrayOutputStream).use { formatter ->
|
||||||
val repo = git.repository
|
val repo = git.repository
|
||||||
|
|
||||||
formatter.setRepository(repo)
|
formatter.setRepository(repo)
|
||||||
|
|
||||||
val oldTree = DirCacheIterator(repo.readDirCache())
|
val oldTree = DirCacheIterator(repo.readDirCache())
|
||||||
@ -34,6 +37,7 @@ class DiffManager @Inject constructor() {
|
|||||||
formatter.scan(oldTree, newTree)
|
formatter.scan(oldTree, newTree)
|
||||||
|
|
||||||
formatter.format(diffEntry)
|
formatter.format(diffEntry)
|
||||||
|
|
||||||
formatter.flush()
|
formatter.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +57,8 @@ class DiffManager @Inject constructor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
suspend fun commitDiffEntries(git: Git, commit: RevCommit): List<DiffEntry> = withContext(Dispatchers.IO) {
|
suspend fun commitDiffEntries(git: Git, commit: RevCommit): List<DiffEntry> = withContext(Dispatchers.IO) {
|
||||||
val repository = git.repository
|
val repository = git.repository
|
||||||
val parent = if (commit.parentCount == 0) {
|
val parent = if (commit.parentCount == 0) {
|
||||||
|
@ -147,10 +147,12 @@ class GitManager @Inject constructor(
|
|||||||
|
|
||||||
fun pull() = managerScope.launch {
|
fun pull() = managerScope.launch {
|
||||||
remoteOperationsManager.pull(safeGit)
|
remoteOperationsManager.pull(safeGit)
|
||||||
|
logManager.loadLog(safeGit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun push() = managerScope.launch {
|
fun push() = managerScope.launch {
|
||||||
remoteOperationsManager.push(safeGit)
|
remoteOperationsManager.push(safeGit)
|
||||||
|
logManager.loadLog(safeGit)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshRepositoryInfo() = managerScope.launch {
|
private fun refreshRepositoryInfo() = managerScope.launch {
|
||||||
@ -163,11 +165,13 @@ class GitManager @Inject constructor(
|
|||||||
fun stash() = managerScope.launch {
|
fun stash() = managerScope.launch {
|
||||||
stashManager.stash(safeGit)
|
stashManager.stash(safeGit)
|
||||||
loadStatus()
|
loadStatus()
|
||||||
|
loadLog()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun popStash() = managerScope.launch {
|
fun popStash() = managerScope.launch {
|
||||||
stashManager.popStash(safeGit)
|
stashManager.popStash(safeGit)
|
||||||
loadStatus()
|
loadStatus()
|
||||||
|
loadLog()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createBranch(branchName: String) = managerScope.launch {
|
fun createBranch(branchName: String) = managerScope.launch {
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
package app.git
|
package app.git
|
||||||
|
|
||||||
|
import app.git.graph.GraphCommitList
|
||||||
|
import app.git.graph.GraphWalk
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.revplot.PlotCommit
|
import org.eclipse.jgit.lib.Constants
|
||||||
import org.eclipse.jgit.revplot.PlotCommitList
|
import org.eclipse.jgit.lib.Ref
|
||||||
import org.eclipse.jgit.revplot.PlotLane
|
|
||||||
import org.eclipse.jgit.revplot.PlotWalk
|
|
||||||
import org.eclipse.jgit.revwalk.RevCommit
|
|
||||||
import org.eclipse.jgit.revwalk.RevWalk
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LogManager @Inject constructor() {
|
|
||||||
|
class LogManager @Inject constructor(
|
||||||
|
private val statusManager: StatusManager,
|
||||||
|
) {
|
||||||
private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading)
|
private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading)
|
||||||
|
|
||||||
val logStatus: StateFlow<LogStatus>
|
val logStatus: StateFlow<LogStatus>
|
||||||
@ -23,21 +24,33 @@ class LogManager @Inject constructor() {
|
|||||||
suspend fun loadLog(git: Git) = withContext(Dispatchers.IO) {
|
suspend fun loadLog(git: Git) = withContext(Dispatchers.IO) {
|
||||||
_logStatus.value = LogStatus.Loading
|
_logStatus.value = LogStatus.Loading
|
||||||
|
|
||||||
val commitList = PlotCommitList<PlotLane>()
|
val logList = git.log().setMaxCount(2).call().toList()
|
||||||
val walk = PlotWalk(git.repository)
|
|
||||||
|
val commitList = GraphCommitList()
|
||||||
|
val walk = GraphWalk(git.repository)
|
||||||
|
|
||||||
|
walk.use {
|
||||||
|
walk.markStartAllRefs(Constants.R_HEADS)
|
||||||
|
walk.markStartAllRefs(Constants.R_REMOTES)
|
||||||
|
walk.markStartAllRefs(Constants.R_TAGS)
|
||||||
|
|
||||||
|
if (statusManager.checkHasUncommitedChanges(git))
|
||||||
|
commitList.addUncommitedChangesGraphCommit(logList.first())
|
||||||
|
|
||||||
walk.markStart(walk.parseCommit(git.repository.resolve("HEAD")));
|
|
||||||
commitList.source(walk)
|
commitList.source(walk)
|
||||||
|
|
||||||
commitList.fillTo(Int.MAX_VALUE)
|
commitList.fillTo(Int.MAX_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
||||||
_logStatus.value = LogStatus.Loaded(commitList)
|
val loadedStatus = LogStatus.Loaded(commitList)
|
||||||
|
|
||||||
|
_logStatus.value = loadedStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sealed class LogStatus {
|
sealed class LogStatus {
|
||||||
object Loading : LogStatus()
|
object Loading : LogStatus()
|
||||||
data class Loaded(val plotCommitList: PlotCommitList<PlotLane>) : LogStatus()
|
class Loaded(val plotCommitList: GraphCommitList) : LogStatus()
|
||||||
}
|
}
|
@ -39,5 +39,7 @@ class RemoteOperationsManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.call()
|
.call()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,13 +22,15 @@ class StatusManager @Inject constructor() {
|
|||||||
get() = _hasUncommitedChanges
|
get() = _hasUncommitedChanges
|
||||||
|
|
||||||
suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) {
|
suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) {
|
||||||
|
_hasUncommitedChanges.value = checkHasUncommitedChanges(git)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun checkHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) {
|
||||||
val status = git
|
val status = git
|
||||||
.status()
|
.status()
|
||||||
.call()
|
.call()
|
||||||
|
|
||||||
val hasUncommitedChanges = status.hasUncommittedChanges() || status.hasUntrackedChanges()
|
return@withContext status.hasUncommittedChanges() || status.hasUntrackedChanges()
|
||||||
|
|
||||||
_hasUncommitedChanges.value = hasUncommitedChanges
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadStatus(git: Git) = withContext(Dispatchers.IO) {
|
suspend fun loadStatus(git: Git) = withContext(Dispatchers.IO) {
|
||||||
|
337
src/main/kotlin/app/git/graph/GraphCommitList.kt
Normal file
337
src/main/kotlin/app/git/graph/GraphCommitList.kt
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
package app.git.graph
|
||||||
|
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommitList
|
||||||
|
import java.lang.ClassCastException
|
||||||
|
import java.text.MessageFormat
|
||||||
|
import org.eclipse.jgit.internal.JGitText
|
||||||
|
import org.eclipse.jgit.lib.AnyObjectId
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk
|
||||||
|
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)
|
||||||
|
|
||||||
|
/** 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) {
|
||||||
|
if(currCommit.id == parentId) {
|
||||||
|
graphCommit.graphParent = currCommit
|
||||||
|
currCommit.addChild(graphCommit)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// 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:
|
||||||
|
var reservedLane: GraphLane? = null
|
||||||
|
var childOnReservedLane: GraphNode? = null
|
||||||
|
var lengthOfReservedLane = -1
|
||||||
|
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 rObj: GraphNode? = get(r)
|
||||||
|
if (rObj === child) {
|
||||||
|
childIndex = r
|
||||||
|
break
|
||||||
|
}
|
||||||
|
addBlockedPosition(blockedPositions, rObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 rObj: GraphNode? = 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.
|
||||||
|
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(
|
||||||
|
Integer.valueOf(
|
||||||
|
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 (r in commitIndex - 1 downTo 0) {
|
||||||
|
val rObj: GraphNode? = get(r)
|
||||||
|
if (rObj === child) break
|
||||||
|
rObj?.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 (l in graphNode.forkingOffLanes) blockedPositions.set(l.position)
|
||||||
|
for (l in graphNode.mergingLanes) blockedPositions.set(l.position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
src/main/kotlin/app/git/graph/GraphLane.kt
Normal file
5
src/main/kotlin/app/git/graph/GraphLane.kt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package app.git.graph
|
||||||
|
|
||||||
|
const val INVALID_LANE_POSITION = -1
|
||||||
|
|
||||||
|
class GraphLane(var position: Int = 0)
|
147
src/main/kotlin/app/git/graph/GraphNode.kt
Normal file
147
src/main/kotlin/app/git/graph/GraphNode.kt
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package app.git.graph
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.AnyObjectId
|
||||||
|
import org.eclipse.jgit.lib.Ref
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
|
|
||||||
|
val NO_CHILDREN = arrayOf<GraphNode>()
|
||||||
|
val NO_LANES = arrayOf<GraphLane>()
|
||||||
|
val NO_REFS = listOf<Ref>()
|
||||||
|
val NO_LANE = GraphLane(INVALID_LANE_POSITION)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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) {
|
||||||
|
when (val childrenCount = children.count()) {
|
||||||
|
0 -> children = arrayOf(c)
|
||||||
|
1 -> if (!c.id.equals(children[0].id)) children = arrayOf(children[0], c)
|
||||||
|
else -> {
|
||||||
|
for (pc in children)
|
||||||
|
if (c.id.equals(pc.id))
|
||||||
|
return
|
||||||
|
|
||||||
|
val n: Array<GraphNode> = children.copyOf(childrenCount + 1).run {
|
||||||
|
this[childrenCount] = c
|
||||||
|
requireNoNulls()
|
||||||
|
}
|
||||||
|
|
||||||
|
n[childrenCount] = c
|
||||||
|
children = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val childCount: Int
|
||||||
|
get() {
|
||||||
|
return children.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the nth child from this commit's child list.
|
||||||
|
*
|
||||||
|
* @param nth
|
||||||
|
* child index to obtain. Must be in the range 0 through
|
||||||
|
* [.getChildCount]-1.
|
||||||
|
* @return the specified child.
|
||||||
|
* @throws ArrayIndexOutOfBoundsException
|
||||||
|
* an invalid child index was specified.
|
||||||
|
*/
|
||||||
|
fun getChild(nth: Int): GraphNode {
|
||||||
|
return children[nth]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the given commit is a child (descendant) of this commit.
|
||||||
|
*
|
||||||
|
* @param c
|
||||||
|
* the commit to test.
|
||||||
|
* @return true if the given commit built on top of this commit.
|
||||||
|
*/
|
||||||
|
fun isChild(c: GraphNode): Boolean {
|
||||||
|
for (a in children)
|
||||||
|
if (a === c)
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of refs for this commit.
|
||||||
|
*
|
||||||
|
* @return number of refs; always a positive value but can be 0.
|
||||||
|
*/
|
||||||
|
fun getRefCount(): Int {
|
||||||
|
return refs.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the nth Ref from this commit's ref list.
|
||||||
|
*
|
||||||
|
* @param nth
|
||||||
|
* ref index to obtain. Must be in the range 0 through
|
||||||
|
* [.getRefCount]-1.
|
||||||
|
* @return the specified ref.
|
||||||
|
* @throws ArrayIndexOutOfBoundsException
|
||||||
|
* an invalid ref index was specified.
|
||||||
|
*/
|
||||||
|
fun getRef(nth: Int): Ref {
|
||||||
|
return refs[nth]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
165
src/main/kotlin/app/git/graph/GraphWalk.kt
Normal file
165
src/main/kotlin/app/git/graph/GraphWalk.kt
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
package app.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
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specialized RevWalk for visualization of a commit graph.
|
||||||
|
*/
|
||||||
|
class GraphWalk(private var repository: Repository?) : RevWalk(repository) {
|
||||||
|
private var additionalRefMap: MutableMap<AnyObjectId, Set<Ref>>?
|
||||||
|
private var reverseRefMap: MutableMap<AnyObjectId, Set<Ref>>? = null
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new revision walker for a given repository.
|
||||||
|
*/
|
||||||
|
init {
|
||||||
|
super.sort(RevSort.TOPO, true)
|
||||||
|
additionalRefMap = HashMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
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)
|
||||||
|
graphNode.refs = getRefs(graphNode)
|
||||||
|
|
||||||
|
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: Any = parseAny(ref.leaf.objectId)
|
||||||
|
|
||||||
|
if (refTarget is RevCommit)
|
||||||
|
markStart(refTarget)
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
src/main/kotlin/app/git/graph/IGraphNode.kt
Normal file
6
src/main/kotlin/app/git/graph/IGraphNode.kt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package app.git.graph
|
||||||
|
|
||||||
|
interface IGraphNode {
|
||||||
|
val graphParentCount: Int
|
||||||
|
fun getGraphParent(nth: Int): GraphNode
|
||||||
|
}
|
17
src/main/kotlin/app/git/graph/UncommitedChangesGraphNode.kt
Normal file
17
src/main/kotlin/app/git/graph/UncommitedChangesGraphNode.kt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package app.git.graph
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.ObjectId
|
||||||
|
|
||||||
|
class UncommitedChangesGraphNode : GraphNode(ObjectId(0, 0, 0, 0, 0)) {
|
||||||
|
|
||||||
|
var graphParent: GraphNode? = null
|
||||||
|
|
||||||
|
override val graphParentCount: Int
|
||||||
|
get() = 1 // TODO: Check what happens with an empty tree
|
||||||
|
|
||||||
|
override fun getGraphParent(nth: Int): GraphNode {
|
||||||
|
return requireNotNull(graphParent)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import app.Main
|
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
fun main() {
|
fun main() {
|
||||||
|
@ -16,8 +16,13 @@ val accentGrayLight = Color(0xFFCCCCCC)
|
|||||||
|
|
||||||
val backgroundColorLight = Color(0xFFEBEFF7)
|
val backgroundColorLight = Color(0xFFEBEFF7)
|
||||||
val surfaceColorLight = Color(0xFFFFFFFF)
|
val surfaceColorLight = Color(0xFFFFFFFF)
|
||||||
|
val surfaceColorDark = Color(0xFF1C1D1E)
|
||||||
val headerBackgroundLight = Color(0xFFF4F6FA)
|
val headerBackgroundLight = Color(0xFFF4F6FA)
|
||||||
|
val headerBackgroundDark = Color(0xFF303132)
|
||||||
|
|
||||||
val addFileLight = Color(0xFF32A852)
|
val addFileLight = Color(0xFF32A852)
|
||||||
val deleteFileLight = errorColor
|
val deleteFileLight = errorColor
|
||||||
val modifyFileLight = primary
|
val modifyFileLight = primary
|
||||||
|
|
||||||
|
val tabColorActiveDark = Color(0xFF606061)
|
||||||
|
val tabColorInactiveDark = Color(0xFF262626)
|
@ -7,7 +7,8 @@ import androidx.compose.ui.graphics.Color
|
|||||||
private val DarkColorPalette = darkColors(
|
private val DarkColorPalette = darkColors(
|
||||||
primary = primaryLight,
|
primary = primaryLight,
|
||||||
primaryVariant = primaryDark,
|
primaryVariant = primaryDark,
|
||||||
secondary = secondary
|
secondary = secondary,
|
||||||
|
surface = surfaceColorDark,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorPalette = lightColors(
|
private val LightColorPalette = lightColors(
|
||||||
@ -23,7 +24,7 @@ private val LightColorPalette = lightColors(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GitnuroTheme(darkTheme: Boolean = false, content: @Composable() () -> Unit) {
|
fun AppTheme(darkTheme: Boolean = false, content: @Composable() () -> Unit) {
|
||||||
val colors = if (darkTheme) {
|
val colors = if (darkTheme) {
|
||||||
DarkColorPalette
|
DarkColorPalette
|
||||||
} else {
|
} else {
|
||||||
@ -38,11 +39,11 @@ fun GitnuroTheme(darkTheme: Boolean = false, content: @Composable() () -> Unit)
|
|||||||
|
|
||||||
@get:Composable
|
@get:Composable
|
||||||
val Colors.primaryTextColor: Color
|
val Colors.primaryTextColor: Color
|
||||||
get() = if(isLight) mainText else mainTextDark
|
get() = if (isLight) mainText else mainTextDark
|
||||||
|
|
||||||
@get:Composable
|
@get:Composable
|
||||||
val Colors.secondaryTextColor: Color
|
val Colors.secondaryTextColor: Color
|
||||||
get() = if(isLight) secondaryText else secondaryTextDark
|
get() = if (isLight) secondaryText else secondaryTextDark
|
||||||
|
|
||||||
@get:Composable
|
@get:Composable
|
||||||
val Colors.accent: Color
|
val Colors.accent: Color
|
||||||
@ -58,7 +59,12 @@ val Colors.accentGray: Color
|
|||||||
|
|
||||||
@get:Composable
|
@get:Composable
|
||||||
val Colors.headerBackground: Color
|
val Colors.headerBackground: Color
|
||||||
get() = headerBackgroundLight
|
get() {
|
||||||
|
return if (isLight)
|
||||||
|
headerBackgroundLight
|
||||||
|
else
|
||||||
|
headerBackgroundDark
|
||||||
|
}
|
||||||
|
|
||||||
@get:Composable
|
@get:Composable
|
||||||
val Colors.addFile: Color
|
val Colors.addFile: Color
|
||||||
@ -71,3 +77,17 @@ val Colors.deleteFile: Color
|
|||||||
@get:Composable
|
@get:Composable
|
||||||
val Colors.modifyFile: Color
|
val Colors.modifyFile: Color
|
||||||
get() = modifyFileLight
|
get() = modifyFileLight
|
||||||
|
|
||||||
|
@get:Composable
|
||||||
|
val Colors.headerText: Color
|
||||||
|
get() = if (isLight) primary else mainTextDark
|
||||||
|
|
||||||
|
|
||||||
|
val Colors.tabColorActive: Color
|
||||||
|
get() = if (isLight) primary else tabColorActiveDark
|
||||||
|
|
||||||
|
|
||||||
|
val Colors.tabColorInactive: Color
|
||||||
|
get() = if (isLight) primaryLight else tabColorInactiveDark
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import app.extensions.simpleName
|
|||||||
import app.git.GitManager
|
import app.git.GitManager
|
||||||
import org.eclipse.jgit.lib.Ref
|
import org.eclipse.jgit.lib.Ref
|
||||||
import app.theme.headerBackground
|
import app.theme.headerBackground
|
||||||
|
import app.theme.headerText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Branches(gitManager: GitManager) {
|
fun Branches(gitManager: GitManager) {
|
||||||
@ -44,7 +45,7 @@ fun Branches(gitManager: GitManager) {
|
|||||||
text = "Local branches",
|
text = "Local branches",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = MaterialTheme.colors.primary,
|
color = MaterialTheme.colors.headerText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
|
@ -31,6 +31,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import app.ui.components.ScrollableLazyColumn
|
import app.ui.components.ScrollableLazyColumn
|
||||||
import app.git.GitManager
|
import app.git.GitManager
|
||||||
|
import app.theme.headerText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CommitChanges(
|
fun CommitChanges(
|
||||||
@ -142,7 +143,7 @@ fun CommitChanges(
|
|||||||
text = "Files changed",
|
text = "Files changed",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = MaterialTheme.colors.primary,
|
color = MaterialTheme.colors.headerText,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
)
|
)
|
||||||
@ -173,11 +174,17 @@ fun rememberNetworkImage(url: String): ImageBitmap {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
LaunchedEffect(url) {
|
LaunchedEffect(url) {
|
||||||
|
try {
|
||||||
loadImage(url).let {
|
loadImage(url).let {
|
||||||
image = makeFromEncoded(it).asImageBitmap()
|
image = makeFromEncoded(it).asImageBitmap()
|
||||||
}
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
println("Avatar loading failed: ${ex.message}")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
@ -6,33 +6,36 @@ import androidx.compose.foundation.gestures.draggable
|
|||||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
import androidx.compose.ui.input.pointer.PointerIcon
|
import androidx.compose.ui.input.pointer.PointerIcon
|
||||||
import androidx.compose.ui.input.pointer.pointerIcon
|
import androidx.compose.ui.input.pointer.pointerIcon
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import app.extensions.reflectForkingOffLanes
|
import app.extensions.simpleName
|
||||||
import app.extensions.reflectMergingLanes
|
|
||||||
import app.extensions.toSmartSystemString
|
import app.extensions.toSmartSystemString
|
||||||
import app.git.GitManager
|
import app.git.GitManager
|
||||||
import app.git.LogStatus
|
import app.git.LogStatus
|
||||||
|
import app.git.graph.GraphNode
|
||||||
import app.theme.headerBackground
|
import app.theme.headerBackground
|
||||||
|
import app.theme.headerText
|
||||||
import app.theme.primaryTextColor
|
import app.theme.primaryTextColor
|
||||||
import app.theme.secondaryTextColor
|
import app.theme.secondaryTextColor
|
||||||
import app.ui.components.ScrollableLazyColumn
|
import app.ui.components.ScrollableLazyColumn
|
||||||
import org.eclipse.jgit.revplot.PlotCommit
|
import org.eclipse.jgit.lib.ObjectIdRef
|
||||||
import org.eclipse.jgit.revplot.PlotCommitList
|
import org.eclipse.jgit.lib.Ref
|
||||||
import org.eclipse.jgit.revplot.PlotLane
|
|
||||||
import org.eclipse.jgit.revwalk.RevCommit
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
|
|
||||||
private val colors = listOf(
|
private val colors = listOf(
|
||||||
@ -100,7 +103,7 @@ fun Log(
|
|||||||
.padding(start = 8.dp),
|
.padding(start = 8.dp),
|
||||||
text = "Graph",
|
text = "Graph",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colors.primary,
|
color = MaterialTheme.colors.headerText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
@ -117,7 +120,7 @@ fun Log(
|
|||||||
.width(graphWidth),
|
.width(graphWidth),
|
||||||
text = "Message",
|
text = "Message",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colors.primary,
|
color = MaterialTheme.colors.headerText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
@ -179,6 +182,9 @@ fun Log(
|
|||||||
}
|
}
|
||||||
|
|
||||||
itemsIndexed(items = commitList) { index, item ->
|
itemsIndexed(items = commitList) { index, item ->
|
||||||
|
val commitRefs = remember(commitList, item) {
|
||||||
|
item.refs
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(40.dp)
|
.height(40.dp)
|
||||||
@ -190,11 +196,9 @@ fun Log(
|
|||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
CommitsGraphLine(
|
CommitsGraphLine(
|
||||||
plotCommit = item,
|
|
||||||
commitList = commitList,
|
|
||||||
hasUncommitedChanges = hasUncommitedChanges,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(graphWidth)
|
.width(graphWidth),
|
||||||
|
plotCommit = item
|
||||||
)
|
)
|
||||||
|
|
||||||
DividerLog(
|
DividerLog(
|
||||||
@ -210,7 +214,8 @@ fun Log(
|
|||||||
CommitMessage(
|
CommitMessage(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
commit = item,
|
commit = item,
|
||||||
selected = selectedIndex.value == index
|
selected = selectedIndex.value == index,
|
||||||
|
refs = commitRefs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -220,7 +225,12 @@ fun Log(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CommitMessage(modifier: Modifier = Modifier, commit: RevCommit, selected: Boolean) {
|
fun CommitMessage(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
commit: RevCommit,
|
||||||
|
selected: Boolean,
|
||||||
|
refs: List<Ref>
|
||||||
|
) {
|
||||||
val textColor = if (selected) {
|
val textColor = if (selected) {
|
||||||
MaterialTheme.colors.primary
|
MaterialTheme.colors.primary
|
||||||
} else
|
} else
|
||||||
@ -241,7 +251,13 @@ fun CommitMessage(modifier: Modifier = Modifier, commit: RevCommit, selected: Bo
|
|||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
refs.forEach {
|
||||||
|
RefChip(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 4.dp),
|
||||||
|
ref = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = commit.shortMessage,
|
text = commit.shortMessage,
|
||||||
@ -291,20 +307,14 @@ fun DividerLog(modifier: Modifier) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun CommitsGraphLine(
|
fun CommitsGraphLine(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
commitList: PlotCommitList<PlotLane>,
|
plotCommit: GraphNode,
|
||||||
plotCommit: PlotCommit<PlotLane>,
|
|
||||||
hasUncommitedChanges: Boolean,
|
|
||||||
) {
|
) {
|
||||||
val passingLanes = remember(plotCommit) {
|
val passingLanes = remember(plotCommit) {
|
||||||
val passingLanesList = mutableListOf<PlotLane>()
|
plotCommit.passingLanes
|
||||||
commitList.findPassingThrough(plotCommit, passingLanesList)
|
|
||||||
|
|
||||||
passingLanesList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val forkingOffLanes = remember(plotCommit) { plotCommit.forkingOffLanes }
|
||||||
val forkingOffLanes = remember(plotCommit) { plotCommit.reflectForkingOffLanes }
|
val mergingLanes = remember(plotCommit) { plotCommit.mergingLanes }
|
||||||
val mergingLanes = remember(plotCommit) { plotCommit.reflectMergingLanes }
|
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
Canvas(
|
Canvas(
|
||||||
@ -313,7 +323,7 @@ fun CommitsGraphLine(
|
|||||||
) {
|
) {
|
||||||
val itemPosition = plotCommit.lane.position
|
val itemPosition = plotCommit.lane.position
|
||||||
clipRect {
|
clipRect {
|
||||||
if (plotCommit.childCount > 0 || hasUncommitedChanges) {
|
if (plotCommit.childCount > 0) {
|
||||||
drawLine(
|
drawLine(
|
||||||
color = colors[itemPosition % colors.size],
|
color = colors[itemPosition % colors.size],
|
||||||
start = Offset(20f * (itemPosition + 1), this.center.y),
|
start = Offset(20f * (itemPosition + 1), this.center.y),
|
||||||
@ -391,3 +401,36 @@ fun UncommitedChangesGraphLine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RefChip(modifier: Modifier = Modifier, ref: Ref) {
|
||||||
|
val icon = remember(ref) {
|
||||||
|
if(ref is ObjectIdRef.PeeledTag) {
|
||||||
|
"tag.svg"
|
||||||
|
} else
|
||||||
|
"branch.svg"
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(MaterialTheme.colors.primary),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 6.dp, end = 4.dp, top = 6.dp, bottom = 6.dp)
|
||||||
|
.size(14.dp),
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.onPrimary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = ref.simpleName,
|
||||||
|
color = MaterialTheme.colors.onPrimary,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 6.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ import app.git.GitManager
|
|||||||
import app.git.StashStatus
|
import app.git.StashStatus
|
||||||
import org.eclipse.jgit.revwalk.RevCommit
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
import app.theme.headerBackground
|
import app.theme.headerBackground
|
||||||
|
import app.theme.headerText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Stashes(gitManager: GitManager) {
|
fun Stashes(gitManager: GitManager) {
|
||||||
@ -46,7 +47,7 @@ fun Stashes(gitManager: GitManager) {
|
|||||||
text = "Stashes",
|
text = "Stashes",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = MaterialTheme.colors.primary,
|
color = MaterialTheme.colors.headerText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
|
@ -32,6 +32,7 @@ import app.git.GitManager
|
|||||||
import app.git.StageStatus
|
import app.git.StageStatus
|
||||||
import org.eclipse.jgit.diff.DiffEntry
|
import org.eclipse.jgit.diff.DiffEntry
|
||||||
import app.theme.headerBackground
|
import app.theme.headerBackground
|
||||||
|
import app.theme.headerText
|
||||||
|
|
||||||
@OptIn(ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -179,7 +180,7 @@ private fun EntriesList(
|
|||||||
text = title,
|
text = title,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = MaterialTheme.colors.primary,
|
color = MaterialTheme.colors.headerText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
|
@ -15,6 +15,9 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.theme.primaryLight
|
import app.theme.primaryLight
|
||||||
|
import app.theme.primaryTextColor
|
||||||
|
import app.theme.tabColorActive
|
||||||
|
import app.theme.tabColorInactive
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -125,19 +128,24 @@ fun TabPanel(
|
|||||||
@Composable
|
@Composable
|
||||||
fun Tab(title: MutableState<String>, selected: Boolean, onClick: () -> Unit, onCloseTab: () -> Unit) {
|
fun Tab(title: MutableState<String>, selected: Boolean, onClick: () -> Unit, onCloseTab: () -> Unit) {
|
||||||
Card {
|
Card {
|
||||||
|
val backgroundColor = if (selected)
|
||||||
|
MaterialTheme.colors.tabColorActive
|
||||||
|
else
|
||||||
|
MaterialTheme.colors.tabColorInactive
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 1.dp)
|
.padding(horizontal = 1.dp)
|
||||||
.height(36.dp)
|
.height(36.dp)
|
||||||
.clip(RoundedCornerShape(5.dp))
|
.clip(RoundedCornerShape(5.dp))
|
||||||
.background(if (selected) MaterialTheme.colors.primary else primaryLight)
|
.background(backgroundColor)
|
||||||
.clickable { onClick() },
|
.clickable { onClick() },
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = title.value,
|
text = title.value,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 8.dp, horizontal = 32.dp),
|
.padding(vertical = 8.dp, horizontal = 32.dp),
|
||||||
color = contentColorFor(MaterialTheme.colors.primary),
|
color = contentColorFor(backgroundColor),
|
||||||
)
|
)
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onCloseTab,
|
onClick = onCloseTab,
|
||||||
|
1
src/main/resources/tag.svg
Normal file
1
src/main/resources/tag.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg>
|
After Width: | Height: | Size: 406 B |
Loading…
Reference in New Issue
Block a user