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.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")
}

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

View file

@ -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),
)
}
}
}
}