From fff18b7fefd3a57f90e6aa2f1c55e67a7c6e6f93 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Sun, 6 Feb 2022 20:49:54 +0100 Subject: [PATCH] Added uncommited files count to "uncommited changes" line in the log Also improved log performance when a file has changed to only update the header and not the whole list --- .../kotlin/app/extensions/ListExtensions.kt | 5 + src/main/kotlin/app/git/StatusManager.kt | 73 +++++++++--- src/main/kotlin/app/git/TabState.kt | 5 - src/main/kotlin/app/ui/log/Log.kt | 112 ++++++++++++++---- .../kotlin/app/viewmodels/LogViewModel.kt | 56 ++++++++- .../kotlin/app/viewmodels/StatusViewModel.kt | 16 --- .../kotlin/app/viewmodels/TabViewModel.kt | 13 +- 7 files changed, 205 insertions(+), 75 deletions(-) create mode 100644 src/main/kotlin/app/extensions/ListExtensions.kt diff --git a/src/main/kotlin/app/extensions/ListExtensions.kt b/src/main/kotlin/app/extensions/ListExtensions.kt new file mode 100644 index 0000000..b3471d6 --- /dev/null +++ b/src/main/kotlin/app/extensions/ListExtensions.kt @@ -0,0 +1,5 @@ +package app.extensions + +fun List?.countOrZero(): Int { + return this?.count() ?: 0 +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/StatusManager.kt b/src/main/kotlin/app/git/StatusManager.kt index 825eb70..a1d7206 100644 --- a/src/main/kotlin/app/git/StatusManager.kt +++ b/src/main/kotlin/app/git/StatusManager.kt @@ -98,8 +98,8 @@ class StatusManager @Inject constructor( val dirCache = repository.lockDirCache() val dirCacheEditor = dirCache.editor() var completedWithErrors = true - try { + try { val rawFileManager = rawFileManagerFactory.create(git.repository) val entryContent = rawFileManager.getRawContent(DiffEntry.Side.NEW, diffEntry) @@ -140,8 +140,6 @@ class StatusManager @Inject constructor( dirCacheEditor.commit() completedWithErrors = false - -// loadStatus(git) } finally { if (completedWithErrors) dirCache.unlock() @@ -208,8 +206,6 @@ class StatusManager @Inject constructor( .checkout() .addPath(diffEntry.filePath) .call() - -// loadStatus(git) } suspend fun unstageAll(git: Git) = withContext(Dispatchers.IO) { @@ -227,8 +223,9 @@ class StatusManager @Inject constructor( suspend fun getStaged(git: Git, currentBranch: Ref?, repositoryState: RepositoryState) = withContext(Dispatchers.IO) { - return@withContext git - .diff() + val statusEntries: List + + val status = git.diff() .setShowNameAndStatusOnly(true).apply { if (currentBranch == null && !repositoryState.isMerging && !repositoryState.isRebasing) setOldTree(EmptyTreeIterator()) // Required if the repository is empty @@ -236,22 +233,28 @@ class StatusManager @Inject constructor( setCached(true) } .call() - // TODO: Grouping and fitlering allows us to remove duplicates when conflicts appear, requires more testing (what happens in windows? /dev/null is a unix thing) - // TODO: Test if we should group by old path or new path - .groupBy { + + statusEntries = if(repositoryState.isMerging || repositoryState.isRebasing) { + status.groupBy { if (it.newPath != "/dev/null") it.newPath else it.oldPath } - .map { - val entries = it.value + .map { + val entries = it.value - val hasConflicts = - (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing)) + val hasConflicts = (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing)) - StatusEntry(entries.first(), isConflict = hasConflicts) + StatusEntry(entries.first(), isConflict = hasConflicts) + } + } else { + status.map { + StatusEntry(it, isConflict = false) } + } + + return@withContext statusEntries } suspend fun getUnstaged(git: Git, repositoryState: RepositoryState) = withContext(Dispatchers.IO) { @@ -274,6 +277,42 @@ class StatusManager @Inject constructor( StatusEntry(entries.first(), isConflict = hasConflicts) } } + + suspend fun getStatusSummary(git: Git, currentBranch: Ref?, repositoryState: RepositoryState): StatusSummary { + val staged = getStaged(git, currentBranch, repositoryState) + val allChanges = staged.toMutableList() + println("Staged: $staged") + + val unstaged = getUnstaged(git, repositoryState) + println("Unstaged: $unstaged") + + allChanges.addAll(unstaged) + val groupedChanges = allChanges.groupBy { + if (it.diffEntry.newPath != "/dev/null") + it.diffEntry.newPath + else + it.diffEntry.oldPath + } + val changesGrouped = groupedChanges.map { + it.value + }.flatten() + .groupBy { + it.diffEntry.changeType + } + + val deletedCount = changesGrouped[DiffEntry.ChangeType.DELETE].countOrZero() + val addedCount = changesGrouped[DiffEntry.ChangeType.ADD].countOrZero() + + val modifiedCount = changesGrouped[DiffEntry.ChangeType.MODIFY].countOrZero() + + changesGrouped[DiffEntry.ChangeType.RENAME].countOrZero() + + changesGrouped[DiffEntry.ChangeType.COPY].countOrZero() + + return StatusSummary( + modifiedCount = modifiedCount, + deletedCount = deletedCount, + addedCount = addedCount, + ) + } } @@ -293,4 +332,6 @@ data class StatusEntry(val diffEntry: DiffEntry, val isConflict: Boolean) { else diffEntry.iconColor } -} \ No newline at end of file +} + +data class StatusSummary(val modifiedCount: Int, val deletedCount: Int, val addedCount: Int) \ No newline at end of file diff --git a/src/main/kotlin/app/git/TabState.kt b/src/main/kotlin/app/git/TabState.kt index efbabd0..bc5e5e0 100644 --- a/src/main/kotlin/app/git/TabState.kt +++ b/src/main/kotlin/app/git/TabState.kt @@ -134,10 +134,5 @@ enum class RefreshType { NONE, ALL_DATA, ONLY_LOG, - - /** - * Requires to update the status if currently selected and update the log if there has been a change - * in the "uncommited changes" state (if there were changes before but not anymore and vice-versa) - */ UNCOMMITED_CHANGES, } \ No newline at end of file diff --git a/src/main/kotlin/app/ui/log/Log.kt b/src/main/kotlin/app/ui/log/Log.kt index 7715b9a..6030cb1 100644 --- a/src/main/kotlin/app/ui/log/Log.kt +++ b/src/main/kotlin/app/ui/log/Log.kt @@ -14,6 +14,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -22,6 +26,7 @@ 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.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.PointerIconDefaults import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.res.painterResource @@ -31,6 +36,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.extensions.* +import app.git.StatusSummary import app.git.graph.GraphNode import app.theme.* import app.ui.SelectedItem @@ -131,6 +137,7 @@ fun Log( UncommitedChangesLine( selected = selectedItem == SelectedItem.UncommitedChanges, hasPreviousCommits = commitList.isNotEmpty(), + statusSummary = logStatus.statusSummary, graphWidth = graphWidth, weightMod = weightMod, repositoryState = repositoryState, @@ -273,7 +280,8 @@ fun UncommitedChangesLine( graphWidth: Dp, weightMod: MutableState, onUncommitedChangesSelected: () -> Unit, - repositoryState: RepositoryState + repositoryState: RepositoryState, + statusSummary: StatusSummary ) { val textColor = if (selected) { MaterialTheme.colors.primary @@ -287,8 +295,9 @@ fun UncommitedChangesLine( .clickable { onUncommitedChangesSelected() }, + verticalAlignment = Alignment.CenterVertically, ) { - UncommitedChangesGraphLine( + UncommitedChangesGraphNode( modifier = Modifier .width(graphWidth), hasPreviousCommits = hasPreviousCommits, @@ -304,29 +313,82 @@ fun UncommitedChangesLine( ) ) - Column( - modifier = Modifier.fillMaxHeight(), - verticalArrangement = Arrangement.Center, - ) { - Spacer(modifier = Modifier.weight(2f)) - - val text = when { - repositoryState.isRebasing -> "Pending changes to rebase" - repositoryState.isMerging -> "Pending changes to merge" - else -> "Uncommited changes" - } - - Text( - text = text, - fontStyle = FontStyle.Italic, - modifier = Modifier.padding(start = 16.dp), - fontSize = 14.sp, - maxLines = 1, - color = textColor, - ) - - Spacer(modifier = Modifier.weight(2f)) + val text = when { + repositoryState.isRebasing -> "Pending changes to rebase" + repositoryState.isMerging -> "Pending changes to merge" + else -> "Uncommited changes" } + + Text( + text = text, + fontStyle = FontStyle.Italic, + modifier = Modifier.padding(start = 16.dp), + fontSize = 14.sp, + maxLines = 1, + color = textColor, + ) + + Spacer(modifier = Modifier.weight(1f)) + + LogStatusSummary( + statusSummary = statusSummary, + modifier = Modifier.padding(horizontal = 8.dp), + ) + } +} + +@Composable +fun LogStatusSummary(statusSummary: StatusSummary, modifier: Modifier) { + Row( + modifier = modifier, + ) { + if (statusSummary.modifiedCount > 0) { + SummaryEntry( + count = statusSummary.modifiedCount, + icon = Icons.Default.Edit, + color = MaterialTheme.colors.modifyFile, + ) + } + + if (statusSummary.addedCount > 0) { + SummaryEntry( + count = statusSummary.addedCount, + icon = Icons.Default.Add, + color = MaterialTheme.colors.addFile, + ) + } + + if (statusSummary.deletedCount > 0) { + SummaryEntry( + count = statusSummary.deletedCount, + icon = Icons.Default.Delete, + color = MaterialTheme.colors.deleteFile, + ) + } + } +} + +@Composable +fun SummaryEntry( + count: Int, + icon: ImageVector, + color: Color +) { + Row( + modifier = Modifier.padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = count.toString(), + color = MaterialTheme.colors.primaryTextColor, + ) + + Icon( + imageVector = icon, + tint = color, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) } } @@ -623,7 +685,7 @@ fun CommitNode( } @Composable -fun UncommitedChangesGraphLine( +fun UncommitedChangesGraphNode( modifier: Modifier = Modifier, hasPreviousCommits: Boolean, ) { diff --git a/src/main/kotlin/app/viewmodels/LogViewModel.kt b/src/main/kotlin/app/viewmodels/LogViewModel.kt index 6f6a304..eac523a 100644 --- a/src/main/kotlin/app/viewmodels/LogViewModel.kt +++ b/src/main/kotlin/app/viewmodels/LogViewModel.kt @@ -16,6 +16,7 @@ class LogViewModel @Inject constructor( private val rebaseManager: RebaseManager, private val tagsManager: TagsManager, private val mergeManager: MergeManager, + private val repositoryManager: RepositoryManager, private val tabState: TabState, ) { private val _logStatus = MutableStateFlow(LogStatus.Loading) @@ -23,13 +24,23 @@ class LogViewModel @Inject constructor( val logStatus: StateFlow get() = _logStatus - suspend fun loadLog(git: Git) { + private suspend fun loadLog(git: Git) { _logStatus.value = LogStatus.Loading val currentBranch = branchesManager.currentBranchRef(git) val log = logManager.loadLog(git, currentBranch) val hasUncommitedChanges = statusManager.hasUncommitedChanges(git) - _logStatus.value = LogStatus.Loaded(hasUncommitedChanges, log, currentBranch) + + val statsSummary = if (hasUncommitedChanges) { + statusManager.getStatusSummary( + git = git, + currentBranch = currentBranch, + repositoryState = repositoryManager.getRepositoryState(git), + ) + } else + StatusSummary(0, 0, 0) + + _logStatus.value = LogStatus.Loaded(hasUncommitedChanges, log, currentBranch, statsSummary) } fun checkoutCommit(revCommit: RevCommit) = tabState.safeProcessing( @@ -56,7 +67,7 @@ class LogViewModel @Inject constructor( branchesManager.checkoutRef(git, ref) } - fun cherrypickCommit(revCommit: RevCommit) = tabState.safeProcessing ( + fun cherrypickCommit(revCommit: RevCommit) = tabState.safeProcessing( refreshType = RefreshType.ONLY_LOG, ) { git -> mergeManager.cherryPickCommit(git, revCommit) @@ -92,6 +103,37 @@ class LogViewModel @Inject constructor( tagsManager.deleteTag(git, tag) } + suspend fun refreshUncommitedChanges(git: Git) { + uncommitedChangesLoadLog(git) + } + + private suspend fun uncommitedChangesLoadLog(git: Git) { + val currentBranch = branchesManager.currentBranchRef(git) + val hasUncommitedChanges = statusManager.hasUncommitedChanges(git) + + val statsSummary = if (hasUncommitedChanges) { + statusManager.getStatusSummary( + git = git, + currentBranch = currentBranch, + repositoryState = repositoryManager.getRepositoryState(git), + ) + } else + StatusSummary(0, 0, 0) + + val previousLogStatusValue = _logStatus.value + + if(previousLogStatusValue is LogStatus.Loaded) { + val newLogStatusValue = LogStatus.Loaded( + hasUncommitedChanges = hasUncommitedChanges, + plotCommitList = previousLogStatusValue.plotCommitList, + currentBranch = currentBranch, + statusSummary = statsSummary, + ) + + _logStatus.value = newLogStatusValue + } + } + suspend fun refresh(git: Git) { loadLog(git) } @@ -105,6 +147,10 @@ class LogViewModel @Inject constructor( sealed class LogStatus { object Loading : LogStatus() - class Loaded(val hasUncommitedChanges: Boolean, val plotCommitList: GraphCommitList, val currentBranch: Ref?) : - LogStatus() + class Loaded( + val hasUncommitedChanges: Boolean, + val plotCommitList: GraphCommitList, + val currentBranch: Ref?, + val statusSummary: StatusSummary, + ) : LogStatus() } \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/StatusViewModel.kt b/src/main/kotlin/app/viewmodels/StatusViewModel.kt index 7b0ac5d..b9a5f9c 100644 --- a/src/main/kotlin/app/viewmodels/StatusViewModel.kt +++ b/src/main/kotlin/app/viewmodels/StatusViewModel.kt @@ -108,22 +108,6 @@ class StatusViewModel @Inject constructor( loadHasUncommitedChanges(git) } - /** - * Checks if there are uncommited changes and returns if the state has changed ( - */ - suspend fun updateHasUncommitedChanges(git: Git): Boolean { - val hadUncommitedChanges = this.lastUncommitedChangesState - - loadStatus(git) - loadHasUncommitedChanges(git) - - val hasNowUncommitedChanges = this.lastUncommitedChangesState - hasPreviousCommits = logManager.hasPreviousCommits(git) - - // Return true to update the log only if the uncommitedChanges status has changed - return (hasNowUncommitedChanges != hadUncommitedChanges) - } - fun continueRebase() = tabState.safeProcessing( refreshType = RefreshType.ALL_DATA, ) { git -> diff --git a/src/main/kotlin/app/viewmodels/TabViewModel.kt b/src/main/kotlin/app/viewmodels/TabViewModel.kt index 839d848..0064c76 100644 --- a/src/main/kotlin/app/viewmodels/TabViewModel.kt +++ b/src/main/kotlin/app/viewmodels/TabViewModel.kt @@ -75,7 +75,7 @@ class TabViewModel @Inject constructor( RefreshType.NONE -> println("Not refreshing...") RefreshType.ALL_DATA -> refreshRepositoryInfo() RefreshType.ONLY_LOG -> refreshLog() - RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges(false) + RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges() } } } @@ -140,21 +140,18 @@ class TabViewModel @Inject constructor( ).collect { if (!tabState.operationRunning) { // Only update if there isn't any process running println("Changes detected, loading status") - checkUncommitedChanges(isFsChange = true) + checkUncommitedChanges() updateDiffEntry() } } } - private suspend fun checkUncommitedChanges(isFsChange: Boolean = false) = tabState.runOperation( + private suspend fun checkUncommitedChanges() = tabState.runOperation( refreshType = RefreshType.NONE, ) { git -> - val uncommitedChangesStateChanged = statusViewModel.updateHasUncommitedChanges(git) - - // Update the log only if the uncommitedChanges status has changed - if ((uncommitedChangesStateChanged && isFsChange) || !isFsChange) - logViewModel.refresh(git) + statusViewModel.refresh(git) + logViewModel.refreshUncommitedChanges(git) updateDiffEntry()