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
This commit is contained in:
Abdelilah El Aissaoui 2022-02-06 20:49:54 +01:00
parent 72e77f41fd
commit fff18b7fef
7 changed files with 205 additions and 75 deletions

View File

@ -0,0 +1,5 @@
package app.extensions
fun <T> List<T>?.countOrZero(): Int {
return this?.count() ?: 0
}

View File

@ -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<StatusEntry>
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
}
}
}
data class StatusSummary(val modifiedCount: Int, val deletedCount: Int, val addedCount: Int)

View File

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

View File

@ -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<Float>,
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,
) {

View File

@ -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>(LogStatus.Loading)
@ -23,13 +24,23 @@ class LogViewModel @Inject constructor(
val logStatus: StateFlow<LogStatus>
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()
}

View File

@ -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 ->

View File

@ -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()