Added new log graph

This commit is contained in:
Abdelilah El Aissaoui 2021-10-08 22:57:18 +02:00
parent 03afe8c1d2
commit 6d0f621bcd
5 changed files with 384 additions and 83 deletions

View file

@ -26,6 +26,7 @@ dependencies {
implementation("org.eclipse.jgit:org.eclipse.jgit:5.13.0.202109080827-r") implementation("org.eclipse.jgit:org.eclipse.jgit:5.13.0.202109080827-r")
implementation("org.apache.sshd:sshd-core:2.7.0") implementation("org.apache.sshd:sshd-core:2.7.0")
implementation("com.google.dagger:dagger:2.39.1") 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") kapt("com.google.dagger:dagger-compiler:2.39.1")
} }

View 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)
}
}
}

View 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()
}

View file

@ -6,11 +6,16 @@ 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.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.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 _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loaded(listOf())) private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading)
val logStatus: StateFlow<LogStatus> val logStatus: StateFlow<LogStatus>
get() = _logStatus get() = _logStatus
@ -18,15 +23,21 @@ 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 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() ensureActive()
_logStatus.value = LogStatus.Loaded(log.toList()) _logStatus.value = LogStatus.Loaded(commitList)
} }
} }
sealed class LogStatus { sealed class LogStatus {
object Loading : LogStatus() object Loading : LogStatus()
data class Loaded(val commits: List<RevCommit>) : LogStatus() data class Loaded(val plotCommitList: PlotCommitList<PlotLane>) : LogStatus()
} }

View file

@ -1,28 +1,57 @@
package app.ui package app.ui
import androidx.compose.foundation.background import androidx.compose.foundation.*
import androidx.compose.foundation.clickable 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.layout.*
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Card import androidx.compose.material.*
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
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.Modifier 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.FontStyle
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.ui.components.ScrollableLazyColumn import app.extensions.reflectForkingOffLanes
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 org.eclipse.jgit.revwalk.RevCommit import app.theme.headerBackground
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.secondaryTextColor 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 @Composable
fun Log( fun Log(
gitManager: GitManager, gitManager: GitManager,
@ -35,11 +64,8 @@ fun Log(
val selectedUncommited = remember { mutableStateOf(false) } val selectedUncommited = remember { mutableStateOf(false) }
val log = if (logStatus is LogStatus.Loaded) { if (logStatus is LogStatus.Loaded) {
logStatus.commits val commitList = logStatus.plotCommitList
} else
listOf()
Card( Card(
modifier = Modifier modifier = Modifier
@ -48,6 +74,11 @@ fun Log(
.fillMaxSize() .fillMaxSize()
) { ) {
val hasUncommitedChanges by gitManager.hasUncommitedChanges.collectAsState() val hasUncommitedChanges by gitManager.hasUncommitedChanges.collectAsState()
var weightMod by remember { mutableStateOf(0f) }
var graphWidth = (CANVAS_MIN_WIDTH + weightMod).dp//(weightMod / 500)
if (graphWidth.value < CANVAS_MIN_WIDTH)
graphWidth = CANVAS_MIN_WIDTH.dp
ScrollableLazyColumn( ScrollableLazyColumn(
modifier = Modifier modifier = Modifier
@ -55,6 +86,44 @@ fun Log(
.fillMaxSize(), .fillMaxSize(),
) { ) {
stickyHeader {
Row(
modifier = Modifier
.fillMaxWidth()
.height(32.dp)
.background(MaterialTheme.colors.headerBackground),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.width(graphWidth)
.padding(start = 8.dp),
text = "Graph",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.primary,
fontSize = 14.sp,
maxLines = 1,
)
DividerLog(
modifier = Modifier.draggable(rememberDraggableState {
weightMod += it
}, Orientation.Horizontal)
)
Text(
modifier = Modifier
.padding(start = 8.dp)
.width(graphWidth),
text = "Message",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.primary,
fontSize = 14.sp,
maxLines = 1,
)
}
}
if (hasUncommitedChanges) if (hasUncommitedChanges)
item { item {
val textColor = if (selectedUncommited.value) { val textColor = if (selectedUncommited.value) {
@ -62,7 +131,7 @@ fun Log(
} else } else
MaterialTheme.colors.primaryTextColor MaterialTheme.colors.primaryTextColor
Column( Row(
modifier = Modifier modifier = Modifier
.height(40.dp) .height(40.dp)
.fillMaxWidth() .fillMaxWidth()
@ -71,6 +140,27 @@ fun Log(
selectedUncommited.value = true selectedUncommited.value = true
onUncommitedChangesSelected() onUncommitedChangesSelected()
}, },
) {
val hasPreviousCommits = remember(commitList) { commitList.count() > 0 }
UncommitedChangesGraphLine(
modifier = Modifier
.width(graphWidth),
hasPreviousCommits = hasPreviousCommits,
)
DividerLog(
modifier = Modifier
.draggable(
rememberDraggableState {
weightMod += it
},
Orientation.Horizontal
)
)
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Spacer(modifier = Modifier.weight(2f)) Spacer(modifier = Modifier.weight(2f))
@ -84,25 +174,11 @@ fun Log(
) )
Spacer(modifier = Modifier.weight(2f)) Spacer(modifier = Modifier.weight(2f))
}
Divider()
} }
} }
itemsIndexed(items = log) { index, item -> itemsIndexed(items = commitList) { index, item ->
val textColor = if (selectedIndex.value == index) {
MaterialTheme.colors.primary
} else
MaterialTheme.colors.primaryTextColor
val secondaryTextColor = if (selectedIndex.value == index) {
MaterialTheme.colors.primary
} else
MaterialTheme.colors.secondaryTextColor
Column {
Spacer(modifier = Modifier.weight(2f))
Row( Row(
modifier = Modifier modifier = Modifier
.height(40.dp) .height(40.dp)
@ -112,12 +188,63 @@ fun Log(
selectedUncommited.value = false selectedUncommited.value = false
onRevCommitSelected(item) onRevCommitSelected(item)
}, },
) {
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
)
}
}
}
}
}
}
@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, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = item.shortMessage, text = commit.shortMessage,
modifier = Modifier.padding(start = 16.dp), modifier = Modifier.padding(start = 16.dp),
fontSize = 14.sp, fontSize = 14.sp,
color = textColor, color = textColor,
@ -127,7 +254,7 @@ fun Log(
Spacer(modifier = Modifier.weight(2f)) Spacer(modifier = Modifier.weight(2f))
Text( Text(
text = item.committerIdent.`when`.toSmartSystemString(), text = commit.committerIdent.`when`.toSmartSystemString(),
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
fontSize = 12.sp, fontSize = 12.sp,
color = secondaryTextColor, color = secondaryTextColor,
@ -137,11 +264,130 @@ fun Log(
} }
Spacer(modifier = Modifier.weight(2f)) Spacer(modifier = Modifier.weight(2f))
Divider()
}
}
}
} }
} }
@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),
)
}
}
}
}