Added new log tree

This commit is contained in:
Abdelilah El Aissaoui 2021-10-15 01:09:25 +02:00
parent 5113ca9a71
commit 87d7f1cdae
24 changed files with 881 additions and 77 deletions

View File

@ -15,7 +15,7 @@ import androidx.compose.ui.zIndex
import app.di.DaggerAppComponent
import app.git.GitManager
import app.git.RepositorySelectionStatus
import app.theme.GitnuroTheme
import app.theme.AppTheme
import app.ui.RepositoryOpenPage
import app.ui.WelcomePage
import app.ui.components.RepositoriesTabPanel
@ -47,7 +47,7 @@ class Main {
size = WindowSize(1280.dp, 720.dp)
)
) {
GitnuroTheme {
AppTheme {
val tabs = remember {
val tabName = mutableStateOf("New tab")
mutableStateOf(
@ -61,7 +61,10 @@ class Main {
}
var selectedTabKey by remember { mutableStateOf(0) }
Column {
Column(
modifier =
Modifier.background(MaterialTheme.colors.surface)
) {
RepositoriesTabPanel(
modifier = Modifier
.padding(top = 4.dp, bottom = 2.dp, start = 4.dp, end = 4.dp)

View File

@ -4,5 +4,8 @@ import org.eclipse.jgit.lib.Ref
val Ref.simpleName: String
get() {
return this.name.split("/").last()
return if (this.name.startsWith("refs/remotes/"))
name.replace("refs/remotes/", "")
else
this.name.split("/").last()
}

View File

@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.ListBranchCommand
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
@ -18,9 +19,7 @@ class BranchesManager @Inject constructor() {
get() = _currentBranch
suspend fun loadBranches(git: Git) = withContext(Dispatchers.IO) {
val branchList = git
.branchList()
.call()
val branchList = getBranches(git)
val branchName = git
.repository
@ -30,6 +29,13 @@ class BranchesManager @Inject constructor() {
_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) {
git
.branchCreate()

View File

@ -5,8 +5,10 @@ import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.diff.DiffFormatter
import org.eclipse.jgit.diff.EditList
import org.eclipse.jgit.dircache.DirCacheIterator
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.patch.HunkHeader
import org.eclipse.jgit.revplot.PlotCommit
import org.eclipse.jgit.revplot.PlotCommitList
import org.eclipse.jgit.revwalk.RevCommit
@ -25,6 +27,7 @@ class DiffManager @Inject constructor() {
DiffFormatter(byteArrayOutputStream).use { formatter ->
val repo = git.repository
formatter.setRepository(repo)
val oldTree = DirCacheIterator(repo.readDirCache())
@ -34,6 +37,7 @@ class DiffManager @Inject constructor() {
formatter.scan(oldTree, newTree)
formatter.format(diffEntry)
formatter.flush()
}
@ -53,6 +57,8 @@ class DiffManager @Inject constructor() {
}
}
suspend fun commitDiffEntries(git: Git, commit: RevCommit): List<DiffEntry> = withContext(Dispatchers.IO) {
val repository = git.repository
val parent = if (commit.parentCount == 0) {

View File

@ -147,10 +147,12 @@ class GitManager @Inject constructor(
fun pull() = managerScope.launch {
remoteOperationsManager.pull(safeGit)
logManager.loadLog(safeGit)
}
fun push() = managerScope.launch {
remoteOperationsManager.push(safeGit)
logManager.loadLog(safeGit)
}
private fun refreshRepositoryInfo() = managerScope.launch {
@ -163,11 +165,13 @@ class GitManager @Inject constructor(
fun stash() = managerScope.launch {
stashManager.stash(safeGit)
loadStatus()
loadLog()
}
fun popStash() = managerScope.launch {
stashManager.popStash(safeGit)
loadStatus()
loadLog()
}
fun createBranch(branchName: String) = managerScope.launch {

View File

@ -1,20 +1,21 @@
package app.git
import app.git.graph.GraphCommitList
import app.git.graph.GraphWalk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
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 org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class LogManager @Inject constructor() {
class LogManager @Inject constructor(
private val statusManager: StatusManager,
) {
private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading)
val logStatus: StateFlow<LogStatus>
@ -23,21 +24,33 @@ class LogManager @Inject constructor() {
suspend fun loadLog(git: Git) = withContext(Dispatchers.IO) {
_logStatus.value = LogStatus.Loading
val commitList = PlotCommitList<PlotLane>()
val walk = PlotWalk(git.repository)
val logList = git.log().setMaxCount(2).call().toList()
walk.markStart(walk.parseCommit(git.repository.resolve("HEAD")));
commitList.source(walk)
val commitList = GraphCommitList()
val walk = GraphWalk(git.repository)
commitList.fillTo(Int.MAX_VALUE)
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())
commitList.source(walk)
commitList.fillTo(Int.MAX_VALUE)
}
ensureActive()
_logStatus.value = LogStatus.Loaded(commitList)
val loadedStatus = LogStatus.Loaded(commitList)
_logStatus.value = loadedStatus
}
}
sealed class LogStatus {
object Loading : LogStatus()
data class Loaded(val plotCommitList: PlotCommitList<PlotLane>) : LogStatus()
class Loaded(val plotCommitList: GraphCommitList) : LogStatus()
}

View File

@ -39,5 +39,7 @@ class RemoteOperationsManager @Inject constructor(
}
}
.call()
}
}

View File

@ -22,13 +22,15 @@ class StatusManager @Inject constructor() {
get() = _hasUncommitedChanges
suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) {
_hasUncommitedChanges.value = checkHasUncommitedChanges(git)
}
suspend fun checkHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) {
val status = git
.status()
.call()
val hasUncommitedChanges = status.hasUncommittedChanges() || status.hasUntrackedChanges()
_hasUncommitedChanges.value = hasUncommitedChanges
return@withContext status.hasUncommittedChanges() || status.hasUntrackedChanges()
}
suspend fun loadStatus(git: Git) = withContext(Dispatchers.IO) {

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

View File

@ -0,0 +1,5 @@
package app.git.graph
const val INVALID_LANE_POSITION = -1
class GraphLane(var position: Int = 0)

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

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

View File

@ -0,0 +1,6 @@
package app.git.graph
interface IGraphNode {
val graphParentCount: Int
fun getGraphParent(nth: Int): GraphNode
}

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

View File

@ -1,5 +1,6 @@
package app
import androidx.compose.ui.ExperimentalComposeUiApi
import app.Main
@OptIn(ExperimentalComposeUiApi::class)
fun main() {

View File

@ -16,8 +16,13 @@ val accentGrayLight = Color(0xFFCCCCCC)
val backgroundColorLight = Color(0xFFEBEFF7)
val surfaceColorLight = Color(0xFFFFFFFF)
val surfaceColorDark = Color(0xFF1C1D1E)
val headerBackgroundLight = Color(0xFFF4F6FA)
val headerBackgroundDark = Color(0xFF303132)
val addFileLight = Color(0xFF32A852)
val deleteFileLight = errorColor
val modifyFileLight = primary
val tabColorActiveDark = Color(0xFF606061)
val tabColorInactiveDark = Color(0xFF262626)

View File

@ -5,25 +5,26 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
private val DarkColorPalette = darkColors(
primary = primaryLight,
primaryVariant = primaryDark,
secondary = secondary
primary = primaryLight,
primaryVariant = primaryDark,
secondary = secondary,
surface = surfaceColorDark,
)
private val LightColorPalette = lightColors(
primary = primary,
primaryVariant = primaryDark,
secondary = secondary,
background = backgroundColorLight,
surface = surfaceColorLight,
error = errorColor
/* Other default colors to override
primary = primary,
primaryVariant = primaryDark,
secondary = secondary,
background = backgroundColorLight,
surface = surfaceColorLight,
error = errorColor
/* Other default colors to override
*/
*/
)
@Composable
fun GitnuroTheme(darkTheme: Boolean = false, content: @Composable() () -> Unit) {
fun AppTheme(darkTheme: Boolean = false, content: @Composable() () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
@ -31,18 +32,18 @@ fun GitnuroTheme(darkTheme: Boolean = false, content: @Composable() () -> Unit)
}
MaterialTheme(
colors = colors,
content = content,
colors = colors,
content = content,
)
}
@get:Composable
val Colors.primaryTextColor: Color
get() = if(isLight) mainText else mainTextDark
get() = if (isLight) mainText else mainTextDark
@get:Composable
val Colors.secondaryTextColor: Color
get() = if(isLight) secondaryText else secondaryTextDark
get() = if (isLight) secondaryText else secondaryTextDark
@get:Composable
val Colors.accent: Color
@ -58,7 +59,12 @@ val Colors.accentGray: Color
@get:Composable
val Colors.headerBackground: Color
get() = headerBackgroundLight
get() {
return if (isLight)
headerBackgroundLight
else
headerBackgroundDark
}
@get:Composable
val Colors.addFile: Color
@ -71,3 +77,17 @@ val Colors.deleteFile: Color
@get:Composable
val Colors.modifyFile: Color
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

View File

@ -23,6 +23,7 @@ import app.extensions.simpleName
import app.git.GitManager
import org.eclipse.jgit.lib.Ref
import app.theme.headerBackground
import app.theme.headerText
@Composable
fun Branches(gitManager: GitManager) {
@ -44,7 +45,7 @@ fun Branches(gitManager: GitManager) {
text = "Local branches",
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.primary,
color = MaterialTheme.colors.headerText,
fontSize = 14.sp,
maxLines = 1,
)

View File

@ -31,11 +31,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import app.ui.components.ScrollableLazyColumn
import app.git.GitManager
import app.theme.headerText
@Composable
fun CommitChanges(
gitManager: GitManager,
commit: RevCommit,
commit: RevCommit,
onDiffSelected: (DiffEntry) -> Unit
) {
var diff by remember { mutableStateOf(emptyList<DiffEntry>()) }
@ -142,7 +143,7 @@ fun CommitChanges(
text = "Files changed",
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.primary,
color = MaterialTheme.colors.headerText,
maxLines = 1,
fontSize = 14.sp,
)
@ -173,12 +174,18 @@ fun rememberNetworkImage(url: String): ImageBitmap {
)
}
LaunchedEffect(url) {
loadImage(url).let {
image = makeFromEncoded(it).asImageBitmap()
try {
loadImage(url).let {
image = makeFromEncoded(it).asImageBitmap()
}
} catch (ex: Exception) {
println("Avatar loading failed: ${ex.message}")
}
}
return image
}

View File

@ -6,33 +6,36 @@ 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.foundation.shape.RoundedCornerShape
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.draw.clip
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.res.painterResource
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.extensions.reflectForkingOffLanes
import app.extensions.reflectMergingLanes
import app.extensions.simpleName
import app.extensions.toSmartSystemString
import app.git.GitManager
import app.git.LogStatus
import app.git.graph.GraphNode
import app.theme.headerBackground
import app.theme.headerText
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.lib.ObjectIdRef
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit
private val colors = listOf(
@ -100,7 +103,7 @@ fun Log(
.padding(start = 8.dp),
text = "Graph",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.primary,
color = MaterialTheme.colors.headerText,
fontSize = 14.sp,
maxLines = 1,
)
@ -117,7 +120,7 @@ fun Log(
.width(graphWidth),
text = "Message",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.primary,
color = MaterialTheme.colors.headerText,
fontSize = 14.sp,
maxLines = 1,
)
@ -179,6 +182,9 @@ fun Log(
}
itemsIndexed(items = commitList) { index, item ->
val commitRefs = remember(commitList, item) {
item.refs
}
Row(
modifier = Modifier
.height(40.dp)
@ -190,11 +196,9 @@ fun Log(
},
) {
CommitsGraphLine(
plotCommit = item,
commitList = commitList,
hasUncommitedChanges = hasUncommitedChanges,
modifier = Modifier
.width(graphWidth)
.width(graphWidth),
plotCommit = item
)
DividerLog(
@ -210,7 +214,8 @@ fun Log(
CommitMessage(
modifier = Modifier.weight(1f),
commit = item,
selected = selectedIndex.value == index
selected = selectedIndex.value == index,
refs = commitRefs,
)
}
}
@ -220,7 +225,12 @@ fun Log(
}
@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) {
MaterialTheme.colors.primary
} else
@ -241,7 +251,13 @@ fun CommitMessage(modifier: Modifier = Modifier, commit: RevCommit, selected: Bo
.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
) {
refs.forEach {
RefChip(
modifier = Modifier
.padding(horizontal = 4.dp),
ref = it,
)
}
Text(
text = commit.shortMessage,
@ -291,20 +307,14 @@ fun DividerLog(modifier: Modifier) {
@Composable
fun CommitsGraphLine(
modifier: Modifier = Modifier,
commitList: PlotCommitList<PlotLane>,
plotCommit: PlotCommit<PlotLane>,
hasUncommitedChanges: Boolean,
plotCommit: GraphNode,
) {
val passingLanes = remember(plotCommit) {
val passingLanesList = mutableListOf<PlotLane>()
commitList.findPassingThrough(plotCommit, passingLanesList)
passingLanesList
plotCommit.passingLanes
}
val forkingOffLanes = remember(plotCommit) { plotCommit.reflectForkingOffLanes }
val mergingLanes = remember(plotCommit) { plotCommit.reflectMergingLanes }
val forkingOffLanes = remember(plotCommit) { plotCommit.forkingOffLanes }
val mergingLanes = remember(plotCommit) { plotCommit.mergingLanes }
Box(modifier = modifier) {
Canvas(
@ -313,7 +323,7 @@ fun CommitsGraphLine(
) {
val itemPosition = plotCommit.lane.position
clipRect {
if (plotCommit.childCount > 0 || hasUncommitedChanges) {
if (plotCommit.childCount > 0) {
drawLine(
color = colors[itemPosition % colors.size],
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)
)
}
}

View File

@ -20,6 +20,7 @@ import app.git.GitManager
import app.git.StashStatus
import org.eclipse.jgit.revwalk.RevCommit
import app.theme.headerBackground
import app.theme.headerText
@Composable
fun Stashes(gitManager: GitManager) {
@ -46,7 +47,7 @@ fun Stashes(gitManager: GitManager) {
text = "Stashes",
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.primary,
color = MaterialTheme.colors.headerText,
fontSize = 14.sp,
maxLines = 1,
)

View File

@ -32,6 +32,7 @@ import app.git.GitManager
import app.git.StageStatus
import org.eclipse.jgit.diff.DiffEntry
import app.theme.headerBackground
import app.theme.headerText
@OptIn(ExperimentalAnimationApi::class)
@Composable
@ -179,7 +180,7 @@ private fun EntriesList(
text = title,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.primary,
color = MaterialTheme.colors.headerText,
fontSize = 14.sp,
maxLines = 1,
)

View File

@ -15,6 +15,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import app.theme.primaryLight
import app.theme.primaryTextColor
import app.theme.tabColorActive
import app.theme.tabColorInactive
@Composable
@ -125,19 +128,24 @@ fun TabPanel(
@Composable
fun Tab(title: MutableState<String>, selected: Boolean, onClick: () -> Unit, onCloseTab: () -> Unit) {
Card {
val backgroundColor = if (selected)
MaterialTheme.colors.tabColorActive
else
MaterialTheme.colors.tabColorInactive
Box(
modifier = Modifier
.padding(horizontal = 1.dp)
.height(36.dp)
.clip(RoundedCornerShape(5.dp))
.background(if (selected) MaterialTheme.colors.primary else primaryLight)
.background(backgroundColor)
.clickable { onClick() },
) {
Text(
text = title.value,
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 32.dp),
color = contentColorFor(MaterialTheme.colors.primary),
color = contentColorFor(backgroundColor),
)
IconButton(
onClick = onCloseTab,

View 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