Added new log graph
This commit is contained in:
parent
03afe8c1d2
commit
6d0f621bcd
5 changed files with 384 additions and 83 deletions
|
@ -26,6 +26,7 @@ dependencies {
|
|||
implementation("org.eclipse.jgit:org.eclipse.jgit:5.13.0.202109080827-r")
|
||||
implementation("org.apache.sshd:sshd-core:2.7.0")
|
||||
implementation("com.google.dagger:dagger:2.39.1")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.21")
|
||||
kapt("com.google.dagger:dagger-compiler:2.39.1")
|
||||
}
|
||||
|
||||
|
|
13
src/main/kotlin/app/CommitNode.kt
Normal file
13
src/main/kotlin/app/CommitNode.kt
Normal file
|
@ -0,0 +1,13 @@
|
|||
package app
|
||||
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
|
||||
class CommitNode(val revCommit: RevCommit) {
|
||||
private val children = mutableListOf<CommitNode>()
|
||||
|
||||
fun addChild(node: CommitNode) {
|
||||
if(children.find { it.revCommit.id == node.revCommit.id } == null) {
|
||||
children.add(node)
|
||||
}
|
||||
}
|
||||
}
|
30
src/main/kotlin/app/extensions/PlotCommit.kt
Normal file
30
src/main/kotlin/app/extensions/PlotCommit.kt
Normal file
|
@ -0,0 +1,30 @@
|
|||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package app.extensions
|
||||
|
||||
import org.eclipse.jgit.revplot.PlotCommit
|
||||
import org.eclipse.jgit.revplot.PlotLane
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
|
||||
val PlotCommit<PlotLane>.reflectForkingOffLanes: Array<PlotLane>
|
||||
get() {
|
||||
val f = PlotCommit::class.memberProperties.find { it.name == "forkingOffLanes" }
|
||||
f?.let {
|
||||
it.isAccessible = true
|
||||
return it.get(this) as Array<PlotLane>
|
||||
}
|
||||
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
val PlotCommit<PlotLane>.reflectMergingLanes: Array<PlotLane>
|
||||
get() {
|
||||
val f = PlotCommit::class.memberProperties.find { it.name == "mergingLanes" }
|
||||
f?.let {
|
||||
it.isAccessible = true
|
||||
return it.get(this) as Array<PlotLane>
|
||||
}
|
||||
|
||||
return emptyArray()
|
||||
}
|
|
@ -6,11 +6,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.revplot.PlotCommit
|
||||
import org.eclipse.jgit.revplot.PlotCommitList
|
||||
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
|
||||
|
||||
class LogManager @Inject constructor() {
|
||||
private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loaded(listOf()))
|
||||
private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading)
|
||||
|
||||
val logStatus: StateFlow<LogStatus>
|
||||
get() = _logStatus
|
||||
|
@ -18,15 +23,21 @@ class LogManager @Inject constructor() {
|
|||
suspend fun loadLog(git: Git) = withContext(Dispatchers.IO) {
|
||||
_logStatus.value = LogStatus.Loading
|
||||
|
||||
val log: Iterable<RevCommit> = git.log().call()
|
||||
val commitList = PlotCommitList<PlotLane>()
|
||||
val walk = PlotWalk(git.repository)
|
||||
|
||||
walk.markStart(walk.parseCommit(git.repository.resolve("HEAD")));
|
||||
commitList.source(walk)
|
||||
|
||||
commitList.fillTo(Int.MAX_VALUE)
|
||||
|
||||
ensureActive()
|
||||
|
||||
_logStatus.value = LogStatus.Loaded(log.toList())
|
||||
_logStatus.value = LogStatus.Loaded(commitList)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class LogStatus {
|
||||
object Loading : LogStatus()
|
||||
data class Loaded(val commits: List<RevCommit>) : LogStatus()
|
||||
data class Loaded(val plotCommitList: PlotCommitList<PlotLane>) : LogStatus()
|
||||
}
|
|
@ -1,28 +1,57 @@
|
|||
package app.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.layout.*
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.ui.components.ScrollableLazyColumn
|
||||
import app.extensions.reflectForkingOffLanes
|
||||
import app.extensions.reflectMergingLanes
|
||||
import app.extensions.toSmartSystemString
|
||||
import app.git.GitManager
|
||||
import app.git.LogStatus
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
import app.theme.headerBackground
|
||||
import app.theme.primaryTextColor
|
||||
import app.theme.secondaryTextColor
|
||||
import app.ui.components.ScrollableLazyColumn
|
||||
import org.eclipse.jgit.revplot.PlotCommit
|
||||
import org.eclipse.jgit.revplot.PlotCommitList
|
||||
import org.eclipse.jgit.revplot.PlotLane
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
|
||||
private val colors = listOf(
|
||||
Color(0xFF42a5f5),
|
||||
Color(0xFFef5350),
|
||||
Color(0xFFe78909c),
|
||||
Color(0xFFff7043),
|
||||
Color(0xFF66bb6a),
|
||||
Color(0xFFec407a),
|
||||
)
|
||||
|
||||
private const val CANVAS_MIN_WIDTH = 100
|
||||
|
||||
// TODO Min size for message column
|
||||
// TODO Horizontal scroll for the graph
|
||||
@OptIn(
|
||||
ExperimentalDesktopApi::class, ExperimentalFoundationApi::class,
|
||||
ExperimentalComposeUiApi::class
|
||||
)
|
||||
@Composable
|
||||
fun Log(
|
||||
gitManager: GitManager,
|
||||
|
@ -35,74 +64,121 @@ fun Log(
|
|||
|
||||
val selectedUncommited = remember { mutableStateOf(false) }
|
||||
|
||||
val log = if (logStatus is LogStatus.Loaded) {
|
||||
logStatus.commits
|
||||
} else
|
||||
listOf()
|
||||
if (logStatus is LogStatus.Loaded) {
|
||||
val commitList = logStatus.plotCommitList
|
||||
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.background(MaterialTheme.colors.surface)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
val hasUncommitedChanges by gitManager.hasUncommitedChanges.collectAsState()
|
||||
|
||||
ScrollableLazyColumn(
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.background(MaterialTheme.colors.surface)
|
||||
.fillMaxSize(),
|
||||
.fillMaxSize()
|
||||
) {
|
||||
val hasUncommitedChanges by gitManager.hasUncommitedChanges.collectAsState()
|
||||
var weightMod by remember { mutableStateOf(0f) }
|
||||
var graphWidth = (CANVAS_MIN_WIDTH + weightMod).dp//(weightMod / 500)
|
||||
|
||||
if (hasUncommitedChanges)
|
||||
item {
|
||||
val textColor = if (selectedUncommited.value) {
|
||||
MaterialTheme.colors.primary
|
||||
} else
|
||||
MaterialTheme.colors.primaryTextColor
|
||||
if (graphWidth.value < CANVAS_MIN_WIDTH)
|
||||
graphWidth = CANVAS_MIN_WIDTH.dp
|
||||
|
||||
Column(
|
||||
ScrollableLazyColumn(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.surface)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(40.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
selectedIndex.value = -1
|
||||
selectedUncommited.value = true
|
||||
onUncommitedChangesSelected()
|
||||
},
|
||||
verticalArrangement = Arrangement.Center,
|
||||
.height(32.dp)
|
||||
.background(MaterialTheme.colors.headerBackground),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
|
||||
Text(
|
||||
text = "Uncommited changes",
|
||||
fontStyle = FontStyle.Italic,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
modifier = Modifier
|
||||
.width(graphWidth)
|
||||
.padding(start = 8.dp),
|
||||
text = "Graph",
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
fontSize = 14.sp,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
DividerLog(
|
||||
modifier = Modifier.draggable(rememberDraggableState {
|
||||
weightMod += it
|
||||
}, Orientation.Horizontal)
|
||||
)
|
||||
|
||||
Divider()
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.width(graphWidth),
|
||||
text = "Message",
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(items = log) { index, item ->
|
||||
val textColor = if (selectedIndex.value == index) {
|
||||
MaterialTheme.colors.primary
|
||||
} else
|
||||
MaterialTheme.colors.primaryTextColor
|
||||
if (hasUncommitedChanges)
|
||||
item {
|
||||
val textColor = if (selectedUncommited.value) {
|
||||
MaterialTheme.colors.primary
|
||||
} else
|
||||
MaterialTheme.colors.primaryTextColor
|
||||
|
||||
val secondaryTextColor = if (selectedIndex.value == index) {
|
||||
MaterialTheme.colors.primary
|
||||
} else
|
||||
MaterialTheme.colors.secondaryTextColor
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(40.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
selectedIndex.value = -1
|
||||
selectedUncommited.value = true
|
||||
onUncommitedChangesSelected()
|
||||
},
|
||||
) {
|
||||
val hasPreviousCommits = remember(commitList) { commitList.count() > 0 }
|
||||
|
||||
UncommitedChangesGraphLine(
|
||||
modifier = Modifier
|
||||
.width(graphWidth),
|
||||
hasPreviousCommits = hasPreviousCommits,
|
||||
)
|
||||
|
||||
Column {
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
DividerLog(
|
||||
modifier = Modifier
|
||||
.draggable(
|
||||
rememberDraggableState {
|
||||
weightMod += it
|
||||
},
|
||||
Orientation.Horizontal
|
||||
)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
|
||||
Text(
|
||||
text = "Uncommited changes",
|
||||
fontStyle = FontStyle.Italic,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
fontSize = 14.sp,
|
||||
color = textColor,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(items = commitList) { index, item ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(40.dp)
|
||||
|
@ -112,36 +188,206 @@ fun Log(
|
|||
selectedUncommited.value = false
|
||||
onRevCommitSelected(item)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
||||
|
||||
Text(
|
||||
text = item.shortMessage,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
fontSize = 14.sp,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
|
||||
Text(
|
||||
text = item.committerIdent.`when`.toSmartSystemString(),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
fontSize = 12.sp,
|
||||
color = secondaryTextColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
CommitsGraphLine(
|
||||
plotCommit = item,
|
||||
commitList = commitList,
|
||||
hasUncommitedChanges = hasUncommitedChanges,
|
||||
modifier = Modifier
|
||||
.width(graphWidth)
|
||||
)
|
||||
|
||||
DividerLog(
|
||||
modifier = Modifier
|
||||
.draggable(
|
||||
rememberDraggableState {
|
||||
weightMod += it
|
||||
},
|
||||
Orientation.Horizontal
|
||||
)
|
||||
)
|
||||
|
||||
CommitMessage(
|
||||
modifier = Modifier.weight(1f),
|
||||
commit = item,
|
||||
selected = selectedIndex.value == index
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
Divider()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CommitMessage(modifier: Modifier = Modifier, commit: RevCommit, selected: Boolean) {
|
||||
val textColor = if (selected) {
|
||||
MaterialTheme.colors.primary
|
||||
} else
|
||||
MaterialTheme.colors.primaryTextColor
|
||||
|
||||
val secondaryTextColor = if (selected) {
|
||||
MaterialTheme.colors.primary
|
||||
} else
|
||||
MaterialTheme.colors.secondaryTextColor
|
||||
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
||||
|
||||
Text(
|
||||
text = commit.shortMessage,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
fontSize = 14.sp,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
|
||||
Text(
|
||||
text = commit.committerIdent.`when`.toSmartSystemString(),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
fontSize = 12.sp,
|
||||
color = secondaryTextColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun DividerLog(modifier: Modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(8.dp)
|
||||
.then(modifier)
|
||||
.pointerIcon(PointerIcon.Hand)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(1.dp)
|
||||
.background(color = MaterialTheme.colors.primary)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun CommitsGraphLine(
|
||||
modifier: Modifier = Modifier,
|
||||
commitList: PlotCommitList<PlotLane>,
|
||||
plotCommit: PlotCommit<PlotLane>,
|
||||
hasUncommitedChanges: Boolean,
|
||||
) {
|
||||
val passingLanes = remember(plotCommit) {
|
||||
val passingLanesList = mutableListOf<PlotLane>()
|
||||
commitList.findPassingThrough(plotCommit, passingLanesList)
|
||||
|
||||
passingLanesList
|
||||
}
|
||||
|
||||
|
||||
val forkingOffLanes = remember(plotCommit) { plotCommit.reflectForkingOffLanes }
|
||||
val mergingLanes = remember(plotCommit) { plotCommit.reflectMergingLanes }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
val itemPosition = plotCommit.lane.position
|
||||
clipRect {
|
||||
if (plotCommit.childCount > 0 || hasUncommitedChanges) {
|
||||
drawLine(
|
||||
color = colors[itemPosition % colors.size],
|
||||
start = Offset(20f * (itemPosition + 1), this.center.y),
|
||||
end = Offset(20f * (itemPosition + 1), 0f),
|
||||
)
|
||||
}
|
||||
|
||||
forkingOffLanes.forEach { plotLane ->
|
||||
drawLine(
|
||||
color = colors[plotLane.position % colors.size],
|
||||
start = Offset(20f * (itemPosition + 1), this.center.y),
|
||||
end = Offset(20f * (plotLane.position + 1), 0f),
|
||||
)
|
||||
}
|
||||
|
||||
mergingLanes.forEach { plotLane ->
|
||||
drawLine(
|
||||
color = colors[plotLane.position % colors.size],
|
||||
start = Offset(20f * (plotLane.position + 1), this.size.height),
|
||||
end = Offset(20f * (itemPosition + 1), this.center.y),
|
||||
)
|
||||
}
|
||||
|
||||
if (plotCommit.parentCount > 0) {
|
||||
drawLine(
|
||||
color = colors[itemPosition % colors.size],
|
||||
start = Offset(20f * (itemPosition + 1), this.center.y),
|
||||
end = Offset(20f * (itemPosition + 1), this.size.height),
|
||||
)
|
||||
}
|
||||
|
||||
passingLanes.forEach { plotLane ->
|
||||
drawLine(
|
||||
color = colors[plotLane.position % colors.size],
|
||||
start = Offset(20f * (plotLane.position + 1), 0f),
|
||||
end = Offset(20f * (plotLane.position + 1), this.size.height),
|
||||
)
|
||||
}
|
||||
|
||||
drawCircle(
|
||||
color = colors[itemPosition % colors.size],
|
||||
radius = 10f,
|
||||
center = Offset(20f * (itemPosition + 1), this.center.y),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UncommitedChangesGraphLine(
|
||||
modifier: Modifier = Modifier,
|
||||
hasPreviousCommits: Boolean,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
clipRect {
|
||||
|
||||
if (hasPreviousCommits)
|
||||
drawLine(
|
||||
color = colors[0],
|
||||
start = Offset(20f, this.center.y),
|
||||
end = Offset(20f, this.size.height),
|
||||
)
|
||||
|
||||
drawCircle(
|
||||
color = colors[0],
|
||||
radius = 10f,
|
||||
center = Offset(20f, this.center.y),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue