Refactored status to improve its performance and removed diff update handling

This commit is contained in:
Abdelilah El Aissaoui 2022-04-06 19:39:06 +02:00
parent 4108537825
commit 1e0660dca0
13 changed files with 200 additions and 149 deletions

View File

@ -0,0 +1,3 @@
package app.exceptions
class MissingDiffEntryException(msg: String) : GitnuroException(msg)

View File

@ -6,6 +6,7 @@ import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import app.git.StatusEntry
import app.git.StatusType import app.git.StatusType
import app.theme.addFile import app.theme.addFile
import app.theme.conflictFile import app.theme.conflictFile
@ -30,6 +31,26 @@ val DiffEntry.parentDirectoryPath: String
"${directoryPath}/" "${directoryPath}/"
} }
val StatusEntry.parentDirectoryPath: String
get() {
val pathSplit = this.filePath.split("/").toMutableList()
pathSplit.removeLast()
val directoryPath = pathSplit.joinToString("/")
return if (directoryPath.isEmpty())
""
else
"${directoryPath}/"
}
val StatusEntry.fileName: String
get() {
val pathSplit = filePath.split("/")
return pathSplit.lastOrNull() ?: ""
}
val DiffEntry.fileName: String val DiffEntry.fileName: String
get() { get() {
val path = if (this.changeType == DiffEntry.ChangeType.DELETE) { val path = if (this.changeType == DiffEntry.ChangeType.DELETE) {
@ -62,6 +83,18 @@ val StatusType.icon: ImageVector
} }
} }
val DiffEntry.icon: ImageVector
get() {
return when (this.changeType) {
DiffEntry.ChangeType.ADD -> Icons.Default.Add
DiffEntry.ChangeType.MODIFY -> Icons.Default.Edit
DiffEntry.ChangeType.DELETE -> Icons.Default.Delete
DiffEntry.ChangeType.COPY -> Icons.Default.Add
DiffEntry.ChangeType.RENAME -> Icons.Default.Refresh
else -> throw NotImplementedError("Unexpected ChangeType")
}
}
val StatusType.iconColor: Color val StatusType.iconColor: Color
@Composable @Composable
get() { get() {
@ -72,3 +105,16 @@ val StatusType.iconColor: Color
StatusType.CONFLICTING -> MaterialTheme.colors.conflictFile StatusType.CONFLICTING -> MaterialTheme.colors.conflictFile
} }
} }
val DiffEntry.iconColor: Color
@Composable
get() {
return when (this.changeType) {
DiffEntry.ChangeType.ADD -> MaterialTheme.colors.addFile
DiffEntry.ChangeType.MODIFY -> MaterialTheme.colors.modifyFile
DiffEntry.ChangeType.DELETE -> MaterialTheme.colors.error
DiffEntry.ChangeType.COPY -> MaterialTheme.colors.addFile
DiffEntry.ChangeType.RENAME -> MaterialTheme.colors.modifyFile
else -> throw NotImplementedError("Unexpected ChangeType")
}
}

View File

@ -2,22 +2,24 @@ package app.git
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
sealed class DiffEntryType(val statusEntry: StatusEntry) { sealed class DiffEntryType() {
class CommitDiff(diffEntry: DiffEntry) : DiffEntryType(diffEntry) class CommitDiff(val diffEntry: DiffEntry) : DiffEntryType()
sealed class UnstagedDiff(diffEntry: DiffEntry) : DiffEntryType(diffEntry) sealed class UncommitedDiff(val statusEntry: StatusEntry) : DiffEntryType()
sealed class StagedDiff(diffEntry: DiffEntry) : DiffEntryType(diffEntry)
sealed class UnstagedDiff(statusEntry: StatusEntry) : UncommitedDiff(statusEntry)
sealed class StagedDiff(statusEntry: StatusEntry) : UncommitedDiff(statusEntry)
/** /**
* State used to represent staged changes when the repository state is not [org.eclipse.jgit.lib.RepositoryState.SAFE] * State used to represent staged changes when the repository state is not [org.eclipse.jgit.lib.RepositoryState.SAFE]
*/ */
class UnsafeStagedDiff(diffEntry: DiffEntry) : StagedDiff(diffEntry) class UnsafeStagedDiff(statusEntry: StatusEntry) : StagedDiff(statusEntry)
/** /**
* State used to represent unstaged changes when the repository state is not [org.eclipse.jgit.lib.RepositoryState.SAFE] * State used to represent unstaged changes when the repository state is not [org.eclipse.jgit.lib.RepositoryState.SAFE]
*/ */
class UnsafeUnstagedDiff(diffEntry: DiffEntry) : UnstagedDiff(diffEntry) class UnsafeUnstagedDiff(statusEntry: StatusEntry) : UnstagedDiff(statusEntry)
class SafeStagedDiff(diffEntry: DiffEntry) : StagedDiff(diffEntry) class SafeStagedDiff(statusEntry: StatusEntry) : StagedDiff(statusEntry)
class SafeUnstagedDiff(diffEntry: DiffEntry) : UnstagedDiff(diffEntry) class SafeUnstagedDiff(statusEntry: StatusEntry) : UnstagedDiff(statusEntry)
} }

View File

@ -2,6 +2,7 @@ package app.git
import app.di.HunkDiffGeneratorFactory import app.di.HunkDiffGeneratorFactory
import app.di.RawFileManagerFactory import app.di.RawFileManagerFactory
import app.exceptions.MissingDiffEntryException
import app.extensions.fullData import app.extensions.fullData
import app.git.diff.DiffResult import app.git.diff.DiffResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -17,6 +18,7 @@ import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.AbstractTreeIterator import org.eclipse.jgit.treewalk.AbstractTreeIterator
import org.eclipse.jgit.treewalk.CanonicalTreeParser import org.eclipse.jgit.treewalk.CanonicalTreeParser
import org.eclipse.jgit.treewalk.FileTreeIterator import org.eclipse.jgit.treewalk.FileTreeIterator
import org.eclipse.jgit.treewalk.filter.PathFilter
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import javax.inject.Inject import javax.inject.Inject
@ -26,9 +28,9 @@ class DiffManager @Inject constructor(
private val hunkDiffGeneratorFactory: HunkDiffGeneratorFactory, private val hunkDiffGeneratorFactory: HunkDiffGeneratorFactory,
) { ) {
suspend fun diffFormat(git: Git, diffEntryType: DiffEntryType): DiffResult = withContext(Dispatchers.IO) { suspend fun diffFormat(git: Git, diffEntryType: DiffEntryType): DiffResult = withContext(Dispatchers.IO) {
val diffEntry = diffEntryType.diffEntry
val byteArrayOutputStream = ByteArrayOutputStream() val byteArrayOutputStream = ByteArrayOutputStream()
val repository = git.repository val repository = git.repository
val diffEntry: DiffEntry
DiffFormatter(byteArrayOutputStream).use { formatter -> DiffFormatter(byteArrayOutputStream).use { formatter ->
formatter.setRepository(repository) formatter.setRepository(repository)
@ -39,6 +41,24 @@ class DiffManager @Inject constructor(
if (diffEntryType is DiffEntryType.UnstagedDiff) if (diffEntryType is DiffEntryType.UnstagedDiff)
formatter.scan(oldTree, newTree) formatter.scan(oldTree, newTree)
diffEntry = when (diffEntryType) {
is DiffEntryType.CommitDiff -> {
diffEntryType.diffEntry
}
is DiffEntryType.UncommitedDiff -> {
val statusEntry = diffEntryType.statusEntry
val firstDiffEntry = git.diff()
.setPathFilter(PathFilter.create(statusEntry.filePath))
.setCached(diffEntryType is DiffEntryType.StagedDiff)
.call()
.firstOrNull()
?: throw MissingDiffEntryException("Diff entry not found")
firstDiffEntry
}
}
formatter.format(diffEntry) formatter.format(diffEntry)
formatter.flush() formatter.flush()

View File

@ -1,8 +1,5 @@
package app.git package app.git
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@ -10,7 +7,6 @@ import app.di.RawFileManagerFactory
import app.extensions.* import app.extensions.*
import app.git.diff.Hunk import app.git.diff.Hunk
import app.git.diff.LineType import app.git.diff.LineType
import app.theme.conflictFile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@ -38,14 +34,14 @@ class StatusManager @Inject constructor(
return@withContext status.hasUncommittedChanges() || status.hasUntrackedChanges() return@withContext status.hasUncommittedChanges() || status.hasUntrackedChanges()
} }
suspend fun stage(git: Git, diffEntry: DiffEntry) = withContext(Dispatchers.IO) { suspend fun stage(git: Git, statusEntry: StatusEntry) = withContext(Dispatchers.IO) {
if (diffEntry.changeType == DiffEntry.ChangeType.DELETE) { if (statusEntry.statusType == StatusType.REMOVED) {
git.rm() git.rm()
.addFilepattern(diffEntry.filePath) .addFilepattern(statusEntry.filePath)
.call() .call()
} else { } else {
git.add() git.add()
.addFilepattern(diffEntry.filePath) .addFilepattern(statusEntry.filePath)
.call() .call()
} }
} }
@ -180,9 +176,9 @@ class StatusManager @Inject constructor(
} }
} }
suspend fun unstage(git: Git, diffEntry: DiffEntry) = withContext(Dispatchers.IO) { suspend fun unstage(git: Git, statusEntry: StatusEntry) = withContext(Dispatchers.IO) {
git.reset() git.reset()
.addPath(diffEntry.filePath) .addPath(statusEntry.filePath)
.call() .call()
} }
@ -194,17 +190,17 @@ class StatusManager @Inject constructor(
.call() .call()
} }
suspend fun reset(git: Git, diffEntry: DiffEntry, staged: Boolean) = withContext(Dispatchers.IO) { suspend fun reset(git: Git, statusEntry: StatusEntry, staged: Boolean) = withContext(Dispatchers.IO) {
if (staged) { if (staged) {
git git
.reset() .reset()
.addPath(diffEntry.filePath) .addPath(statusEntry.filePath)
.call() .call()
} }
git git
.checkout() .checkout()
.addPath(diffEntry.filePath) .addPath(statusEntry.filePath)
.call() .call()
} }
@ -221,7 +217,7 @@ class StatusManager @Inject constructor(
.call() .call()
} }
suspend fun getStaged(git: Git, currentBranch: Ref?, repositoryState: RepositoryState) = suspend fun getStaged(git: Git) =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// TODO Test on an empty repository or with a non-default state like merging or rebasing // TODO Test on an empty repository or with a non-default state like merging or rebasing
@ -275,8 +271,8 @@ class StatusManager @Inject constructor(
) )
} }
suspend fun getStatusSummary(git: Git, currentBranch: Ref?, repositoryState: RepositoryState): StatusSummary { suspend fun getStatusSummary(git: Git): StatusSummary {
val staged = getStaged(git, currentBranch, repositoryState) val staged = getStaged(git)
val allChanges = staged.toMutableList() val allChanges = staged.toMutableList()
val unstaged = getUnstaged(git) val unstaged = getUnstaged(git)
@ -326,7 +322,7 @@ class StatusManager @Inject constructor(
} }
data class StatusEntry(val entry: String, val statusType: StatusType) { data class StatusEntry(val filePath: String, val statusType: StatusType) {
val icon: ImageVector val icon: ImageVector
get() = statusType.icon get() = statusType.icon

View File

@ -50,15 +50,15 @@ class HunkDiffGenerator @AssistedInject constructor(
if (rawOld == EntryContent.InvalidObjectBlob || rawNew == EntryContent.InvalidObjectBlob) if (rawOld == EntryContent.InvalidObjectBlob || rawNew == EntryContent.InvalidObjectBlob)
throw InvalidObjectException("Invalid object in diff format") throw InvalidObjectException("Invalid object in diff format")
var diffResult: DiffResult = DiffResult.Text(emptyList()) var diffResult: DiffResult = DiffResult.Text(ent, emptyList())
// If we can, generate text diff (if one of the files has never been a binary file) // If we can, generate text diff (if one of the files has never been a binary file)
val hasGeneratedTextDiff = canGenerateTextDiff(rawOld, rawNew) { oldRawText, newRawText -> val hasGeneratedTextDiff = canGenerateTextDiff(rawOld, rawNew) { oldRawText, newRawText ->
diffResult = DiffResult.Text(format(fileHeader, oldRawText, newRawText)) diffResult = DiffResult.Text(ent, format(fileHeader, oldRawText, newRawText))
} }
if (!hasGeneratedTextDiff) { if (!hasGeneratedTextDiff) {
diffResult = DiffResult.NonText(rawOld, rawNew) diffResult = DiffResult.NonText(ent, rawOld, rawNew)
} }
return diffResult return diffResult
@ -190,10 +190,17 @@ class HunkDiffGenerator @AssistedInject constructor(
} }
} }
sealed class DiffResult { sealed class DiffResult(
data class Text(val hunks: List<Hunk>) : DiffResult() val diffEntry: DiffEntry,
data class NonText( ) {
class Text(
diffEntry: DiffEntry,
val hunks: List<Hunk>
) : DiffResult(diffEntry)
class NonText(
diffEntry: DiffEntry,
val oldBinaryContent: EntryContent, val oldBinaryContent: EntryContent,
val newBinaryContent: EntryContent, val newBinaryContent: EntryContent,
) : DiffResult() ) : DiffResult(diffEntry)
} }

View File

@ -197,7 +197,7 @@ fun CommitLogChanges(
val textColor: Color val textColor: Color
val secondaryTextColor: Color val secondaryTextColor: Color
if (diffSelected?.diffEntry == diffEntry) { if (diffSelected is DiffEntryType.CommitDiff && diffSelected.diffEntry == diffEntry) {
textColor = MaterialTheme.colors.primary textColor = MaterialTheme.colors.primary
secondaryTextColor = MaterialTheme.colors.halfPrimary secondaryTextColor = MaterialTheme.colors.halfPrimary
} else { } else {

View File

@ -7,9 +7,11 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -23,6 +25,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.git.DiffEntryType import app.git.DiffEntryType
import app.git.EntryContent import app.git.EntryContent
import app.git.StatusType
import app.git.diff.DiffResult import app.git.diff.DiffResult
import app.git.diff.Hunk import app.git.diff.Hunk
import app.git.diff.Line import app.git.diff.Line
@ -33,6 +36,7 @@ import app.theme.unstageButton
import app.ui.components.ScrollableLazyColumn import app.ui.components.ScrollableLazyColumn
import app.ui.components.SecondaryButton import app.ui.components.SecondaryButton
import app.viewmodels.DiffViewModel import app.viewmodels.DiffViewModel
import app.viewmodels.ViewDiffResult
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
import java.io.FileInputStream import java.io.FileInputStream
import java.nio.file.Path import java.nio.file.Path
@ -47,22 +51,33 @@ fun Diff(
val diffResultState = diffViewModel.diffResult.collectAsState() val diffResultState = diffViewModel.diffResult.collectAsState()
val viewDiffResult = diffResultState.value ?: return val viewDiffResult = diffResultState.value ?: return
val diffEntryType = viewDiffResult.diffEntryType
val diffEntry = diffEntryType.diffEntry
val diffResult = viewDiffResult.diffResult
Column( Column(
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
.fillMaxSize() .fillMaxSize()
) { ) {
DiffHeader(diffEntry, onCloseDiffView) when (viewDiffResult) {
if (diffResult is DiffResult.Text) { ViewDiffResult.DiffNotFound -> { onCloseDiffView() }
TextDiff(diffEntryType, diffViewModel, diffResult) is ViewDiffResult.Loaded -> {
} else if (diffResult is DiffResult.NonText) { val diffEntryType = viewDiffResult.diffEntryType
NonTextDiff(diffResult) val diffEntry = viewDiffResult.diffResult.diffEntry
val diffResult = viewDiffResult.diffResult
DiffHeader(diffEntry, onCloseDiffView)
if (diffResult is DiffResult.Text) {
TextDiff(diffEntryType, diffViewModel, diffResult)
} else if (diffResult is DiffResult.NonText) {
NonTextDiff(diffResult)
}
}
ViewDiffResult.Loading -> {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
} }
} }
} }
@ -175,8 +190,9 @@ fun TextDiff(diffEntryType: DiffEntryType, diffViewModel: DiffViewModel, diffRes
DisableSelection { DisableSelection {
HunkHeader( HunkHeader(
hunk = hunk, hunk = hunk,
diffEntryType = diffEntryType,
diffViewModel = diffViewModel, diffViewModel = diffViewModel,
diffEntryType = diffEntryType,
diffEntry =diffResult.diffEntry,
) )
} }
} }
@ -200,6 +216,7 @@ fun HunkHeader(
hunk: Hunk, hunk: Hunk,
diffEntryType: DiffEntryType, diffEntryType: DiffEntryType,
diffViewModel: DiffViewModel, diffViewModel: DiffViewModel,
diffEntry: DiffEntry,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -215,9 +232,12 @@ fun HunkHeader(
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// Hunks options are only visible when repository is a normal state (not during merge/rebase)
if ( if (
(diffEntryType is DiffEntryType.SafeStagedDiff || diffEntryType is DiffEntryType.SafeUnstagedDiff) && (diffEntryType is DiffEntryType.SafeStagedDiff || diffEntryType is DiffEntryType.SafeUnstagedDiff) &&
diffEntryType.diffEntry.changeType == DiffEntry.ChangeType.MODIFY (diffEntryType is DiffEntryType.UncommitedDiff && // Added just to make smartcast work
diffEntryType.statusEntry.statusType == StatusType.MODIFIED)
) { ) {
val buttonText: String val buttonText: String
val color: Color val color: Color
@ -234,9 +254,9 @@ fun HunkHeader(
backgroundButton = color, backgroundButton = color,
onClick = { onClick = {
if (diffEntryType is DiffEntryType.StagedDiff) { if (diffEntryType is DiffEntryType.StagedDiff) {
diffViewModel.unstageHunk(diffEntryType.diffEntry, hunk) diffViewModel.unstageHunk(diffEntry, hunk)
} else { } else {
diffViewModel.stageHunk(diffEntryType.diffEntry, hunk) diffViewModel.stageHunk(diffEntry, hunk)
} }
} }
) )

View File

@ -45,16 +45,16 @@ import app.ui.context_menu.stagedEntriesContextMenuItems
import app.ui.context_menu.unstagedEntriesContextMenuItems import app.ui.context_menu.unstagedEntriesContextMenuItems
import app.viewmodels.StageStatus import app.viewmodels.StageStatus
import app.viewmodels.StatusViewModel import app.viewmodels.StatusViewModel
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.lib.RepositoryState
import kotlin.reflect.KClass
@Composable @Composable
fun UncommitedChanges( fun UncommitedChanges(
statusViewModel: StatusViewModel, statusViewModel: StatusViewModel,
selectedEntryType: DiffEntryType?, selectedEntryType: DiffEntryType?,
repositoryState: RepositoryState, repositoryState: RepositoryState,
onStagedDiffEntrySelected: (DiffEntry?) -> Unit, onStagedDiffEntrySelected: (StatusEntry?) -> Unit,
onUnstagedDiffEntrySelected: (DiffEntry) -> Unit, onUnstagedDiffEntrySelected: (StatusEntry) -> Unit,
) { ) {
val stageStatusState = statusViewModel.stageStatus.collectAsState() val stageStatusState = statusViewModel.stageStatus.collectAsState()
var commitMessage by remember { mutableStateOf(statusViewModel.savedCommitMessage) } var commitMessage by remember { mutableStateOf(statusViewModel.savedCommitMessage) }
@ -66,18 +66,6 @@ fun UncommitedChanges(
if (stageStatus is StageStatus.Loaded) { if (stageStatus is StageStatus.Loaded) {
staged = stageStatus.staged staged = stageStatus.staged
unstaged = stageStatus.unstaged unstaged = stageStatus.unstaged
LaunchedEffect(staged) {
if (selectedEntryType != null) {
checkIfSelectedEntryShouldBeUpdated(
selectedEntryType = selectedEntryType,
staged = staged,
unstaged = unstaged,
onStagedDiffEntrySelected = onStagedDiffEntrySelected,
onUnstagedDiffEntrySelected = onUnstagedDiffEntrySelected,
)
}
}
} else { } else {
staged = listOf() staged = listOf()
unstaged = listOf() // return empty lists if still loading unstaged = listOf() // return empty lists if still loading
@ -110,7 +98,7 @@ fun UncommitedChanges(
title = "Staged", title = "Staged",
allActionTitle = "Unstage all", allActionTitle = "Unstage all",
actionTitle = "Unstage", actionTitle = "Unstage",
selectedEntryType = selectedEntryType, selectedEntryType = if(selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null,
actionColor = MaterialTheme.colors.unstageButton, actionColor = MaterialTheme.colors.unstageButton,
statusEntries = staged, statusEntries = staged,
onDiffEntrySelected = onStagedDiffEntrySelected, onDiffEntrySelected = onStagedDiffEntrySelected,
@ -127,7 +115,7 @@ fun UncommitedChanges(
}, },
onAllAction = { onAllAction = {
statusViewModel.unstageAll() statusViewModel.unstageAll()
} },
) )
EntriesList( EntriesList(
@ -137,20 +125,21 @@ fun UncommitedChanges(
.fillMaxWidth(), .fillMaxWidth(),
title = "Unstaged", title = "Unstaged",
actionTitle = "Stage", actionTitle = "Stage",
selectedEntryType = if(selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null,
actionColor = MaterialTheme.colors.stageButton, actionColor = MaterialTheme.colors.stageButton,
statusEntries = unstaged, statusEntries = unstaged,
onDiffEntrySelected = onUnstagedDiffEntrySelected, onDiffEntrySelected = onUnstagedDiffEntrySelected,
onDiffEntryOptionSelected = { onDiffEntryOptionSelected = {
statusViewModel.stage(it) statusViewModel.stage(it)
}, },
onGenerateContextMenu = { diffEntry -> onGenerateContextMenu = { statusEntry ->
unstagedEntriesContextMenuItems( unstagedEntriesContextMenuItems(
statusEntry = diffEntry, statusEntry = statusEntry,
onReset = { onReset = {
statusViewModel.resetUnstaged(diffEntry) statusViewModel.resetUnstaged(statusEntry)
}, },
onDelete = { onDelete = {
statusViewModel.deleteFile(diffEntry) statusViewModel.deleteFile(statusEntry)
} }
) )
}, },
@ -158,7 +147,6 @@ fun UncommitedChanges(
statusViewModel.stageAll() statusViewModel.stageAll()
}, },
allActionTitle = "Stage all", allActionTitle = "Stage all",
selectedEntryType = selectedEntryType
) )
Column( Column(
@ -392,46 +380,6 @@ fun ConfirmationButton(
} }
} }
// TODO: This logic should be part of the diffViewModel where it gets the latest version of the diffEntry
fun checkIfSelectedEntryShouldBeUpdated(
selectedEntryType: DiffEntryType,
staged: List<StatusEntry>,
unstaged: List<StatusEntry>,
onStagedDiffEntrySelected: (DiffEntry?) -> Unit,
onUnstagedDiffEntrySelected: (DiffEntry) -> Unit,
) {
val selectedDiffEntry = selectedEntryType.diffEntry
val selectedEntryTypeNewId = selectedDiffEntry.newId.name()
if (selectedEntryType is DiffEntryType.StagedDiff) {
val entryType =
staged.firstOrNull { stagedEntry -> stagedEntry.diffEntry.newPath == selectedDiffEntry.newPath }?.diffEntry
if (
entryType != null &&
selectedEntryTypeNewId != entryType.newId.name()
) {
onStagedDiffEntrySelected(entryType)
} else if (entryType == null) {
onStagedDiffEntrySelected(null)
}
} else if (selectedEntryType is DiffEntryType.UnstagedDiff) {
val entryType = unstaged.firstOrNull { unstagedEntry ->
if (selectedDiffEntry.changeType == DiffEntry.ChangeType.DELETE)
unstagedEntry.diffEntry.oldPath == selectedDiffEntry.oldPath
else
unstagedEntry.diffEntry.newPath == selectedDiffEntry.newPath
}
if (entryType != null) {
onUnstagedDiffEntrySelected(entryType.diffEntry)
} else
onStagedDiffEntrySelected(null)
}
}
@OptIn(ExperimentalAnimationApi::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalAnimationApi::class, ExperimentalFoundationApi::class)
@Composable @Composable
private fun EntriesList( private fun EntriesList(
@ -478,17 +426,19 @@ private fun EntriesList(
.background(MaterialTheme.colors.background), .background(MaterialTheme.colors.background),
) { ) {
itemsIndexed(statusEntries) { index, statusEntry -> itemsIndexed(statusEntries) { index, statusEntry ->
val isEntrySelected = selectedEntryType?.diffEntry == diffEntry val isEntrySelected = selectedEntryType != null &&
selectedEntryType is DiffEntryType.UncommitedDiff && // Added for smartcast
selectedEntryType.statusEntry == statusEntry
FileEntry( FileEntry(
statusEntry = statusEntry, statusEntry = statusEntry,
isSelected = isEntrySelected, isSelected = isEntrySelected,
actionTitle = actionTitle, actionTitle = actionTitle,
actionColor = actionColor, actionColor = actionColor,
onClick = { onClick = {
onDiffEntrySelected(diffEntry) onDiffEntrySelected(statusEntry)
}, },
onButtonClick = { onButtonClick = {
onDiffEntryOptionSelected(diffEntry) onDiffEntryOptionSelected(statusEntry)
}, },
onGenerateContextMenu = onGenerateContextMenu, onGenerateContextMenu = onGenerateContextMenu,
) )
@ -513,7 +463,7 @@ private fun FileEntry(
actionColor: Color, actionColor: Color,
onClick: () -> Unit, onClick: () -> Unit,
onButtonClick: () -> Unit, onButtonClick: () -> Unit,
onGenerateContextMenu: (DiffEntry) -> List<ContextMenuItem>, onGenerateContextMenu: (StatusEntry) -> List<ContextMenuItem>,
) { ) {
var active by remember { mutableStateOf(false) } var active by remember { mutableStateOf(false) }
@ -545,7 +495,7 @@ private fun FileEntry(
) { ) {
ContextMenuArea( ContextMenuArea(
items = { items = {
onGenerateContextMenu(diffEntry) onGenerateContextMenu(statusEntry)
}, },
) { ) {
Row( Row(
@ -564,9 +514,9 @@ private fun FileEntry(
tint = statusEntry.iconColor, tint = statusEntry.iconColor,
) )
if(diffEntry.parentDirectoryPath.isNotEmpty()) { if(statusEntry.parentDirectoryPath.isNotEmpty()) {
Text( Text(
text = diffEntry.parentDirectoryPath, text = statusEntry.parentDirectoryPath,
modifier = Modifier.weight(1f, fill = false), modifier = Modifier.weight(1f, fill = false),
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
@ -576,7 +526,7 @@ private fun FileEntry(
) )
} }
Text( Text(
text = diffEntry.fileName, text = statusEntry.fileName,
modifier = Modifier.weight(1f, fill = false), modifier = Modifier.weight(1f, fill = false),
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,

View File

@ -2,15 +2,17 @@ package app.ui.context_menu
import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import app.git.StatusEntry
import app.git.StatusType
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun stagedEntriesContextMenuItems( fun stagedEntriesContextMenuItems(
diffEntry: DiffEntry, diffEntry: StatusEntry,
onReset: () -> Unit, onReset: () -> Unit,
): List<ContextMenuItem> { ): List<ContextMenuItem> {
return mutableListOf<ContextMenuItem>().apply { return mutableListOf<ContextMenuItem>().apply {
if (diffEntry.changeType != DiffEntry.ChangeType.ADD) { if (diffEntry.statusType != StatusType.ADDED) {
add( add(
ContextMenuItem( ContextMenuItem(
label = "Reset", label = "Reset",

View File

@ -1,6 +1,7 @@
package app.viewmodels package app.viewmodels
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import app.exceptions.MissingDiffEntryException
import app.git.* import app.git.*
import app.git.diff.DiffResult import app.git.diff.DiffResult
import app.git.diff.Hunk import app.git.diff.Hunk
@ -15,7 +16,7 @@ class DiffViewModel @Inject constructor(
private val statusManager: StatusManager, private val statusManager: StatusManager,
) { ) {
// TODO Maybe use a sealed class instead of a null to represent that a diff is not selected? // TODO Maybe use a sealed class instead of a null to represent that a diff is not selected?
private val _diffResult = MutableStateFlow<ViewDiffResult?>(null) private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading)
val diffResult: StateFlow<ViewDiffResult?> = _diffResult val diffResult: StateFlow<ViewDiffResult?> = _diffResult
val lazyListState = MutableStateFlow( val lazyListState = MutableStateFlow(
@ -25,18 +26,23 @@ class DiffViewModel @Inject constructor(
) )
) )
// TODO Cancel job if the user closed the diff view while loading
fun updateDiff(diffEntryType: DiffEntryType) = tabState.runOperation( fun updateDiff(diffEntryType: DiffEntryType) = tabState.runOperation(
refreshType = RefreshType.NONE, refreshType = RefreshType.NONE,
) { git -> ) { git ->
val oldDiffEntryType = _diffResult.value?.diffEntryType var oldDiffEntryType: DiffEntryType? = null
val oldDiffResult = _diffResult.value
_diffResult.value = null if(oldDiffResult is ViewDiffResult.Loaded) {
oldDiffEntryType = oldDiffResult.diffEntryType
}
_diffResult.value = ViewDiffResult.Loading
// If it's a different file or different state (index or workdir), reset the scroll state // If it's a different file or different state (index or workdir), reset the scroll state
if (oldDiffEntryType != null && if (oldDiffEntryType != null &&
(oldDiffEntryType.diffEntry.oldPath != diffEntryType.diffEntry.oldPath || oldDiffEntryType is DiffEntryType.UncommitedDiff && diffEntryType is DiffEntryType.UncommitedDiff &&
oldDiffEntryType.diffEntry.newPath != diffEntryType.diffEntry.newPath || oldDiffEntryType.statusEntry.filePath != diffEntryType.statusEntry.filePath
oldDiffEntryType::class != diffEntryType::class)
) { ) {
lazyListState.value = LazyListState( lazyListState.value = LazyListState(
0, 0,
@ -44,13 +50,14 @@ class DiffViewModel @Inject constructor(
) )
} }
//TODO: Just a workaround when trying to diff binary files
try { try {
val hunks = diffManager.diffFormat(git, diffEntryType) val diffFormat = diffManager.diffFormat(git, diffEntryType)
_diffResult.value = ViewDiffResult(diffEntryType, hunks) _diffResult.value = ViewDiffResult.Loaded(diffEntryType, diffFormat)
} catch (ex: Exception) { } catch (ex: Exception) {
ex.printStackTrace() if(ex is MissingDiffEntryException) {
_diffResult.value = ViewDiffResult(diffEntryType, DiffResult.Text(emptyList())) _diffResult.value = ViewDiffResult.DiffNotFound
} else
ex.printStackTrace()
} }
} }
@ -67,4 +74,9 @@ class DiffViewModel @Inject constructor(
} }
} }
data class ViewDiffResult(val diffEntryType: DiffEntryType, val diffResult: DiffResult)
sealed interface ViewDiffResult {
object Loading: ViewDiffResult
object DiffNotFound: ViewDiffResult
data class Loaded(val diffEntryType: DiffEntryType, val diffResult: DiffResult): ViewDiffResult
}

View File

@ -56,8 +56,6 @@ class LogViewModel @Inject constructor(
val statusSummary = statusManager.getStatusSummary( val statusSummary = statusManager.getStatusSummary(
git = git, git = git,
currentBranch = currentBranch,
repositoryState = repositoryManager.getRepositoryState(git),
) )
val hasUncommitedChanges = val hasUncommitedChanges =
@ -163,8 +161,6 @@ class LogViewModel @Inject constructor(
val statsSummary = if (hasUncommitedChanges) { val statsSummary = if (hasUncommitedChanges) {
statusManager.getStatusSummary( statusManager.getStatusSummary(
git = git, git = git,
currentBranch = currentBranch,
repositoryState = repositoryManager.getRepositoryState(git),
) )
} else } else
StatusSummary(0, 0, 0) StatusSummary(0, 0, 0)

View File

@ -6,7 +6,6 @@ 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.diff.DiffEntry
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -27,16 +26,16 @@ class StatusViewModel @Inject constructor(
private var lastUncommitedChangesState = false private var lastUncommitedChangesState = false
fun stage(diffEntry: DiffEntry) = tabState.runOperation( fun stage(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.stage(git, diffEntry) statusManager.stage(git, statusEntry)
} }
fun unstage(diffEntry: DiffEntry) = tabState.runOperation( fun unstage(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.unstage(git, diffEntry) statusManager.unstage(git, statusEntry)
} }
@ -52,16 +51,16 @@ class StatusViewModel @Inject constructor(
statusManager.stageAll(git) statusManager.stageAll(git)
} }
fun resetStaged(diffEntry: DiffEntry) = tabState.runOperation( fun resetStaged(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.reset(git, diffEntry, staged = true) statusManager.reset(git, statusEntry, staged = true)
} }
fun resetUnstaged(diffEntry: DiffEntry) = tabState.runOperation( fun resetUnstaged(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.reset(git, diffEntry, staged = false) statusManager.reset(git, statusEntry, staged = false)
} }
private suspend fun loadStatus(git: Git) { private suspend fun loadStatus(git: Git) {
@ -69,9 +68,7 @@ class StatusViewModel @Inject constructor(
try { try {
_stageStatus.value = StageStatus.Loading _stageStatus.value = StageStatus.Loading
val repositoryState = repositoryManager.getRepositoryState(git) val staged = statusManager.getStaged(git)
val currentBranchRef = branchesManager.currentBranchRef(git)
val staged = statusManager.getStaged(git, currentBranchRef, repositoryState)
val unstaged = statusManager.getUnstaged(git) val unstaged = statusManager.getUnstaged(git)
_stageStatus.value = StageStatus.Loaded(staged, unstaged) _stageStatus.value = StageStatus.Loaded(staged, unstaged)
@ -141,10 +138,10 @@ class StatusViewModel @Inject constructor(
mergeManager.abortMerge(git) mergeManager.abortMerge(git)
} }
fun deleteFile(diffEntry: DiffEntry) = tabState.runOperation( fun deleteFile(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
val path = diffEntry.newPath val path = statusEntry.filePath
val fileToDelete = File(git.repository.directory.parent, path) val fileToDelete = File(git.repository.directory.parent, path)