diff --git a/src/main/kotlin/app/App.kt b/src/main/kotlin/app/App.kt index 188520d..3a59664 100644 --- a/src/main/kotlin/app/App.kt +++ b/src/main/kotlin/app/App.kt @@ -44,8 +44,6 @@ class App { @Inject lateinit var appPreferences: AppPreferences - private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - init { appComponent.inject(this) } @@ -92,7 +90,7 @@ class App { } } } else { - appScope.cancel("Closing app") + appStateManager.cancelCoroutines() this.exitApplication() } } @@ -151,7 +149,7 @@ class App { } } - private fun removeTab(key: Int) = appScope.launch(Dispatchers.IO) { + private fun removeTab(key: Int) = appStateManager.appStateScope.launch(Dispatchers.IO) { // Stop any running jobs val tabs = tabsFlow.value val tabToRemove = tabs.firstOrNull { it.key == key } ?: return@launch @@ -164,7 +162,7 @@ class App { tabsFlow.value = tabsFlow.value.filter { tab -> tab.key != key } } - fun addTab(tabInformation: TabInformation) = appScope.launch(Dispatchers.IO) { + fun addTab(tabInformation: TabInformation) = appStateManager.appStateScope.launch(Dispatchers.IO) { tabsFlow.value = tabsFlow.value.toMutableList().apply { add(tabInformation) } } diff --git a/src/main/kotlin/app/AppStateManager.kt b/src/main/kotlin/app/AppStateManager.kt index 1246267..79482a5 100644 --- a/src/main/kotlin/app/AppStateManager.kt +++ b/src/main/kotlin/app/AppStateManager.kt @@ -19,7 +19,7 @@ class AppStateManager @Inject constructor( val latestOpenedRepositoriesPaths: List get() = _latestOpenedRepositoriesPaths - private val appStateScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) // TODO Stop this when closing the app + val appStateScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) val latestOpenedRepositoryPath: String get() = _latestOpenedRepositoriesPaths.firstOrNull() ?: "" @@ -74,4 +74,8 @@ class AppStateManager @Inject constructor( _latestOpenedRepositoriesPaths.addAll(repositories) } } + + fun cancelCoroutines() { + appStateScope.cancel("Closing app") + } } \ No newline at end of file diff --git a/src/main/kotlin/app/exceptions/MissingDiffEntryException.kt b/src/main/kotlin/app/exceptions/MissingDiffEntryException.kt new file mode 100644 index 0000000..294a3ee --- /dev/null +++ b/src/main/kotlin/app/exceptions/MissingDiffEntryException.kt @@ -0,0 +1,3 @@ +package app.exceptions + +class MissingDiffEntryException(msg: String) : GitnuroException(msg) \ No newline at end of file diff --git a/src/main/kotlin/app/extensions/DiffEntryExtensions.kt b/src/main/kotlin/app/extensions/DiffEntryExtensions.kt index a4b1ce2..e037eeb 100644 --- a/src/main/kotlin/app/extensions/DiffEntryExtensions.kt +++ b/src/main/kotlin/app/extensions/DiffEntryExtensions.kt @@ -2,14 +2,14 @@ package app.extensions import androidx.compose.material.MaterialTheme 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.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.* import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import app.git.StatusEntry +import app.git.StatusType import app.theme.addFile +import app.theme.conflictFile import app.theme.modifyFile import org.eclipse.jgit.diff.DiffEntry @@ -31,6 +31,26 @@ val DiffEntry.parentDirectoryPath: String "${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 get() { val path = if (this.changeType == DiffEntry.ChangeType.DELETE) { @@ -53,6 +73,16 @@ val DiffEntry.filePath: String return path } +val StatusType.icon: ImageVector + get() { + return when (this) { + StatusType.ADDED -> Icons.Default.Add + StatusType.MODIFIED -> Icons.Default.Edit + StatusType.REMOVED -> Icons.Default.Delete + StatusType.CONFLICTING -> Icons.Default.Warning + } + } + val DiffEntry.icon: ImageVector get() { return when (this.changeType) { @@ -65,6 +95,17 @@ val DiffEntry.icon: ImageVector } } +val StatusType.iconColor: Color + @Composable + get() { + return when (this) { + StatusType.ADDED -> MaterialTheme.colors.addFile + StatusType.MODIFIED -> MaterialTheme.colors.modifyFile + StatusType.REMOVED -> MaterialTheme.colors.error + StatusType.CONFLICTING -> MaterialTheme.colors.conflictFile + } + } + val DiffEntry.iconColor: Color @Composable get() { diff --git a/src/main/kotlin/app/extensions/ListExtensions.kt b/src/main/kotlin/app/extensions/ListExtensions.kt index b3471d6..0806271 100644 --- a/src/main/kotlin/app/extensions/ListExtensions.kt +++ b/src/main/kotlin/app/extensions/ListExtensions.kt @@ -2,4 +2,14 @@ package app.extensions fun List?.countOrZero(): Int { return this?.count() ?: 0 +} + +fun flatListOf(vararg lists: List): List { + val flatList = mutableListOf() + + for(list in lists) { + flatList.addAll(list) + } + + return flatList } \ No newline at end of file diff --git a/src/main/kotlin/app/git/DiffEntryType.kt b/src/main/kotlin/app/git/DiffEntryType.kt index f4d2ed3..a5d967b 100644 --- a/src/main/kotlin/app/git/DiffEntryType.kt +++ b/src/main/kotlin/app/git/DiffEntryType.kt @@ -2,22 +2,24 @@ package app.git import org.eclipse.jgit.diff.DiffEntry -sealed class DiffEntryType(val diffEntry: DiffEntry) { - class CommitDiff(diffEntry: DiffEntry) : DiffEntryType(diffEntry) +sealed class DiffEntryType() { + class CommitDiff(val diffEntry: DiffEntry) : DiffEntryType() - sealed class UnstagedDiff(diffEntry: DiffEntry) : DiffEntryType(diffEntry) - sealed class StagedDiff(diffEntry: DiffEntry) : DiffEntryType(diffEntry) + sealed class UncommitedDiff(val statusEntry: StatusEntry) : DiffEntryType() + + 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] */ - 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] */ - class UnsafeUnstagedDiff(diffEntry: DiffEntry) : UnstagedDiff(diffEntry) + class UnsafeUnstagedDiff(statusEntry: StatusEntry) : UnstagedDiff(statusEntry) - class SafeStagedDiff(diffEntry: DiffEntry) : StagedDiff(diffEntry) - class SafeUnstagedDiff(diffEntry: DiffEntry) : UnstagedDiff(diffEntry) + class SafeStagedDiff(statusEntry: StatusEntry) : StagedDiff(statusEntry) + class SafeUnstagedDiff(statusEntry: StatusEntry) : UnstagedDiff(statusEntry) } diff --git a/src/main/kotlin/app/git/DiffManager.kt b/src/main/kotlin/app/git/DiffManager.kt index eb1abe6..95c89c2 100644 --- a/src/main/kotlin/app/git/DiffManager.kt +++ b/src/main/kotlin/app/git/DiffManager.kt @@ -2,6 +2,7 @@ package app.git import app.di.HunkDiffGeneratorFactory import app.di.RawFileManagerFactory +import app.exceptions.MissingDiffEntryException import app.extensions.fullData import app.git.diff.DiffResult 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.CanonicalTreeParser import org.eclipse.jgit.treewalk.FileTreeIterator +import org.eclipse.jgit.treewalk.filter.PathFilter import java.io.ByteArrayOutputStream import javax.inject.Inject @@ -26,9 +28,9 @@ class DiffManager @Inject constructor( private val hunkDiffGeneratorFactory: HunkDiffGeneratorFactory, ) { suspend fun diffFormat(git: Git, diffEntryType: DiffEntryType): DiffResult = withContext(Dispatchers.IO) { - val diffEntry = diffEntryType.diffEntry val byteArrayOutputStream = ByteArrayOutputStream() val repository = git.repository + val diffEntry: DiffEntry DiffFormatter(byteArrayOutputStream).use { formatter -> formatter.setRepository(repository) @@ -39,6 +41,24 @@ class DiffManager @Inject constructor( if (diffEntryType is DiffEntryType.UnstagedDiff) 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.flush() diff --git a/src/main/kotlin/app/git/StatusManager.kt b/src/main/kotlin/app/git/StatusManager.kt index 8746fe1..6a6c4ea 100644 --- a/src/main/kotlin/app/git/StatusManager.kt +++ b/src/main/kotlin/app/git/StatusManager.kt @@ -1,8 +1,5 @@ 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.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector @@ -10,17 +7,18 @@ import app.di.RawFileManagerFactory import app.extensions.* import app.git.diff.Hunk import app.git.diff.LineType -import app.theme.conflictFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.Status import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.RawText import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit import org.eclipse.jgit.dircache.DirCacheEntry -import org.eclipse.jgit.lib.* -import org.eclipse.jgit.submodule.SubmoduleStatusType -import org.eclipse.jgit.treewalk.EmptyTreeIterator +import org.eclipse.jgit.lib.Constants +import org.eclipse.jgit.lib.FileMode +import org.eclipse.jgit.lib.ObjectInserter +import org.eclipse.jgit.lib.Repository import java.io.ByteArrayInputStream import java.io.IOException import java.nio.ByteBuffer @@ -40,14 +38,14 @@ class StatusManager @Inject constructor( return@withContext status.hasUncommittedChanges() || status.hasUntrackedChanges() } - suspend fun stage(git: Git, diffEntry: DiffEntry) = withContext(Dispatchers.IO) { - if (diffEntry.changeType == DiffEntry.ChangeType.DELETE) { + suspend fun stage(git: Git, statusEntry: StatusEntry) = withContext(Dispatchers.IO) { + if (statusEntry.statusType == StatusType.REMOVED) { git.rm() - .addFilepattern(diffEntry.filePath) + .addFilepattern(statusEntry.filePath) .call() } else { git.add() - .addFilepattern(diffEntry.filePath) + .addFilepattern(statusEntry.filePath) .call() } } @@ -132,7 +130,7 @@ class StatusManager @Inject constructor( // Restore previously removed lines to the index for (line in removedLines) { // Check how many lines before this one have been deleted - val previouslyRemovedLines = addedLines.count { it.newLineNumber <= line.newLineNumber } - 1 + val previouslyRemovedLines = addedLines.count { it.newLineNumber < line.newLineNumber } textLines.add(line.newLineNumber + linesAdded - previouslyRemovedLines, line.text.withoutLineEnding) linesAdded++ } @@ -182,9 +180,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() - .addPath(diffEntry.filePath) + .addPath(statusEntry.filePath) .call() } @@ -196,17 +194,17 @@ class StatusManager @Inject constructor( .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) { git .reset() - .addPath(diffEntry.filePath) + .addPath(statusEntry.filePath) .call() } git .checkout() - .addPath(diffEntry.filePath) + .addPath(statusEntry.filePath) .call() } @@ -223,99 +221,86 @@ class StatusManager @Inject constructor( .call() } - suspend fun getStaged(git: Git, currentBranch: Ref?, repositoryState: RepositoryState) = + suspend fun getStatus(git: Git) = withContext(Dispatchers.IO) { - 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 - - setCached(true) - } + git + .status() .call() - - statusEntries = if (repositoryState.isMerging || repositoryState.isRebasing) { - status.groupBy { - if (it.newPath != "/dev/null") - it.newPath - else - it.oldPath - } - .map { - val entries = it.value - - val hasConflicts = - (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing)) - - 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) { - val uninitializedSubmodules = submodulesManager.uninitializedSubmodules(git) - - return@withContext git - .diff() - .call() - .filter { - !uninitializedSubmodules.containsKey(it.oldPath) // Filter out uninitialized modules directories + suspend fun getStaged(status: Status) = + withContext(Dispatchers.IO) { + val added = status.added.map { + StatusEntry(it, StatusType.ADDED) } - .groupBy { - if (it.oldPath != "/dev/null") - it.oldPath - else - it.newPath + val modified = status.changed.map { + StatusEntry(it, StatusType.MODIFIED) } - .map { - val entries = it.value - - val hasConflicts = - (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing)) - - StatusEntry(entries.first(), isConflict = hasConflicts) + val removed = status.removed.map { + StatusEntry(it, StatusType.REMOVED) } + + return@withContext flatListOf( + added, + modified, + removed, + ) + } + + suspend fun getUnstaged(status: Status) = withContext(Dispatchers.IO) { + // TODO Test uninitialized modules after the refactor +// val uninitializedSubmodules = submodulesManager.uninitializedSubmodules(git) + + val added = status.untracked.map { + StatusEntry(it, StatusType.ADDED) + } + val modified = status.modified.map { + StatusEntry(it, StatusType.MODIFIED) + } + val removed = status.missing.map { + StatusEntry(it, StatusType.REMOVED) + } + val conflicting = status.conflicting.map { + StatusEntry(it, StatusType.CONFLICTING) + } + + return@withContext flatListOf( + added, + modified, + removed, + conflicting, + ) } - suspend fun getStatusSummary(git: Git, currentBranch: Ref?, repositoryState: RepositoryState): StatusSummary { - val staged = getStaged(git, currentBranch, repositoryState) + suspend fun getStatusSummary(git: Git): StatusSummary { + val status = getStatus(git) + val staged = getStaged(status) val allChanges = staged.toMutableList() - val unstaged = getUnstaged(git, repositoryState) + val unstaged = getUnstaged(status) 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 + it.statusType } - val deletedCount = changesGrouped[DiffEntry.ChangeType.DELETE].countOrZero() - val addedCount = changesGrouped[DiffEntry.ChangeType.ADD].countOrZero() + val deletedCount = changesGrouped[StatusType.REMOVED].countOrZero() + val addedCount = changesGrouped[StatusType.ADDED].countOrZero() - val modifiedCount = changesGrouped[DiffEntry.ChangeType.MODIFY].countOrZero() + - changesGrouped[DiffEntry.ChangeType.RENAME].countOrZero() + - changesGrouped[DiffEntry.ChangeType.COPY].countOrZero() + val modifiedCount = changesGrouped[StatusType.MODIFIED].countOrZero() + val conflictingCount = changesGrouped[StatusType.CONFLICTING].countOrZero() return StatusSummary( modifiedCount = modifiedCount, deletedCount = deletedCount, addedCount = addedCount, + conflictingCount = conflictingCount, ) } @@ -341,22 +326,30 @@ class StatusManager @Inject constructor( } -data class StatusEntry(val diffEntry: DiffEntry, val isConflict: Boolean) { +data class StatusEntry(val filePath: String, val statusType: StatusType) { val icon: ImageVector - get() { - return if (isConflict) - Icons.Default.Warning - else - diffEntry.icon - } + get() = statusType.icon + val iconColor: Color @Composable - get() { - return if (isConflict) - MaterialTheme.colors.conflictFile - else - diffEntry.iconColor - } + get() = statusType.iconColor } -data class StatusSummary(val modifiedCount: Int, val deletedCount: Int, val addedCount: Int) \ No newline at end of file +enum class StatusType { + ADDED, + MODIFIED, + REMOVED, + CONFLICTING, +} + +data class StatusSummary( + val modifiedCount: Int, + val deletedCount: Int, + val addedCount: Int, + val conflictingCount: Int, +) { + val total = modifiedCount + + deletedCount + + addedCount + + conflictingCount +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt b/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt index 7bc021e..3da8feb 100644 --- a/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt +++ b/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt @@ -50,15 +50,15 @@ class HunkDiffGenerator @AssistedInject constructor( if (rawOld == EntryContent.InvalidObjectBlob || rawNew == EntryContent.InvalidObjectBlob) 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) val hasGeneratedTextDiff = canGenerateTextDiff(rawOld, rawNew) { oldRawText, newRawText -> - diffResult = DiffResult.Text(format(fileHeader, oldRawText, newRawText)) + diffResult = DiffResult.Text(ent, format(fileHeader, oldRawText, newRawText)) } if (!hasGeneratedTextDiff) { - diffResult = DiffResult.NonText(rawOld, rawNew) + diffResult = DiffResult.NonText(ent, rawOld, rawNew) } return diffResult @@ -190,10 +190,17 @@ class HunkDiffGenerator @AssistedInject constructor( } } -sealed class DiffResult { - data class Text(val hunks: List) : DiffResult() - data class NonText( +sealed class DiffResult( + val diffEntry: DiffEntry, +) { + class Text( + diffEntry: DiffEntry, + val hunks: List + ) : DiffResult(diffEntry) + + class NonText( + diffEntry: DiffEntry, val oldBinaryContent: EntryContent, val newBinaryContent: EntryContent, - ) : DiffResult() + ) : DiffResult(diffEntry) } \ No newline at end of file diff --git a/src/main/kotlin/app/ui/CommitChanges.kt b/src/main/kotlin/app/ui/CommitChanges.kt index 9707cad..3f2d657 100644 --- a/src/main/kotlin/app/ui/CommitChanges.kt +++ b/src/main/kotlin/app/ui/CommitChanges.kt @@ -197,7 +197,7 @@ fun CommitLogChanges( val textColor: Color val secondaryTextColor: Color - if (diffSelected?.diffEntry == diffEntry) { + if (diffSelected is DiffEntryType.CommitDiff && diffSelected.diffEntry == diffEntry) { textColor = MaterialTheme.colors.primary secondaryTextColor = MaterialTheme.colors.halfPrimary } else { diff --git a/src/main/kotlin/app/ui/Diff.kt b/src/main/kotlin/app/ui/Diff.kt index 8b22925..bc1b6b8 100644 --- a/src/main/kotlin/app/ui/Diff.kt +++ b/src/main/kotlin/app/ui/Diff.kt @@ -7,9 +7,11 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.IconButton +import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -23,6 +25,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.git.DiffEntryType import app.git.EntryContent +import app.git.StatusType import app.git.diff.DiffResult import app.git.diff.Hunk import app.git.diff.Line @@ -33,6 +36,7 @@ import app.theme.unstageButton import app.ui.components.ScrollableLazyColumn import app.ui.components.SecondaryButton import app.viewmodels.DiffViewModel +import app.viewmodels.ViewDiffResult import org.eclipse.jgit.diff.DiffEntry import java.io.FileInputStream import java.nio.file.Path @@ -47,22 +51,33 @@ fun Diff( val diffResultState = diffViewModel.diffResult.collectAsState() val viewDiffResult = diffResultState.value ?: return - val diffEntryType = viewDiffResult.diffEntryType - val diffEntry = diffEntryType.diffEntry - val diffResult = viewDiffResult.diffResult - Column( modifier = Modifier .padding(8.dp) .background(MaterialTheme.colors.background) .fillMaxSize() ) { - DiffHeader(diffEntry, onCloseDiffView) - if (diffResult is DiffResult.Text) { - TextDiff(diffEntryType, diffViewModel, diffResult) - } else if (diffResult is DiffResult.NonText) { - NonTextDiff(diffResult) + when (viewDiffResult) { + ViewDiffResult.DiffNotFound -> { onCloseDiffView() } + is ViewDiffResult.Loaded -> { + val diffEntryType = viewDiffResult.diffEntryType + 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 { HunkHeader( hunk = hunk, - diffEntryType = diffEntryType, diffViewModel = diffViewModel, + diffEntryType = diffEntryType, + diffEntry =diffResult.diffEntry, ) } } @@ -200,6 +216,7 @@ fun HunkHeader( hunk: Hunk, diffEntryType: DiffEntryType, diffViewModel: DiffViewModel, + diffEntry: DiffEntry, ) { Row( modifier = Modifier @@ -215,9 +232,12 @@ fun HunkHeader( ) Spacer(modifier = Modifier.weight(1f)) + + // Hunks options are only visible when repository is a normal state (not during merge/rebase) if ( (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 color: Color @@ -234,9 +254,9 @@ fun HunkHeader( backgroundButton = color, onClick = { if (diffEntryType is DiffEntryType.StagedDiff) { - diffViewModel.unstageHunk(diffEntryType.diffEntry, hunk) + diffViewModel.unstageHunk(diffEntry, hunk) } else { - diffViewModel.stageHunk(diffEntryType.diffEntry, hunk) + diffViewModel.stageHunk(diffEntry, hunk) } } ) diff --git a/src/main/kotlin/app/ui/UncommitedChanges.kt b/src/main/kotlin/app/ui/UncommitedChanges.kt index 03ceab5..9965da5 100644 --- a/src/main/kotlin/app/ui/UncommitedChanges.kt +++ b/src/main/kotlin/app/ui/UncommitedChanges.kt @@ -45,16 +45,16 @@ import app.ui.context_menu.stagedEntriesContextMenuItems import app.ui.context_menu.unstagedEntriesContextMenuItems import app.viewmodels.StageStatus import app.viewmodels.StatusViewModel -import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.lib.RepositoryState +import kotlin.reflect.KClass @Composable fun UncommitedChanges( statusViewModel: StatusViewModel, selectedEntryType: DiffEntryType?, repositoryState: RepositoryState, - onStagedDiffEntrySelected: (DiffEntry?) -> Unit, - onUnstagedDiffEntrySelected: (DiffEntry) -> Unit, + onStagedDiffEntrySelected: (StatusEntry?) -> Unit, + onUnstagedDiffEntrySelected: (StatusEntry) -> Unit, ) { val stageStatusState = statusViewModel.stageStatus.collectAsState() var commitMessage by remember { mutableStateOf(statusViewModel.savedCommitMessage) } @@ -66,18 +66,6 @@ fun UncommitedChanges( if (stageStatus is StageStatus.Loaded) { staged = stageStatus.staged unstaged = stageStatus.unstaged - - LaunchedEffect(staged) { - if (selectedEntryType != null) { - checkIfSelectedEntryShouldBeUpdated( - selectedEntryType = selectedEntryType, - staged = staged, - unstaged = unstaged, - onStagedDiffEntrySelected = onStagedDiffEntrySelected, - onUnstagedDiffEntrySelected = onUnstagedDiffEntrySelected, - ) - } - } } else { staged = listOf() unstaged = listOf() // return empty lists if still loading @@ -110,9 +98,9 @@ fun UncommitedChanges( title = "Staged", allActionTitle = "Unstage all", actionTitle = "Unstage", - selectedEntryType = selectedEntryType, + selectedEntryType = if(selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null, actionColor = MaterialTheme.colors.unstageButton, - diffEntries = staged, + statusEntries = staged, onDiffEntrySelected = onStagedDiffEntrySelected, onDiffEntryOptionSelected = { statusViewModel.unstage(it) @@ -127,7 +115,7 @@ fun UncommitedChanges( }, onAllAction = { statusViewModel.unstageAll() - } + }, ) EntriesList( @@ -137,20 +125,21 @@ fun UncommitedChanges( .fillMaxWidth(), title = "Unstaged", actionTitle = "Stage", + selectedEntryType = if(selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null, actionColor = MaterialTheme.colors.stageButton, - diffEntries = unstaged, + statusEntries = unstaged, onDiffEntrySelected = onUnstagedDiffEntrySelected, onDiffEntryOptionSelected = { statusViewModel.stage(it) }, - onGenerateContextMenu = { diffEntry -> + onGenerateContextMenu = { statusEntry -> unstagedEntriesContextMenuItems( - diffEntry = diffEntry, + statusEntry = statusEntry, onReset = { - statusViewModel.resetUnstaged(diffEntry) + statusViewModel.resetUnstaged(statusEntry) }, onDelete = { - statusViewModel.deleteFile(diffEntry) + statusViewModel.deleteFile(statusEntry) } ) }, @@ -158,7 +147,6 @@ fun UncommitedChanges( statusViewModel.stageAll() }, allActionTitle = "Stage all", - selectedEntryType = selectedEntryType ) 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, - unstaged: List, - 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) @Composable private fun EntriesList( @@ -439,10 +387,10 @@ private fun EntriesList( title: String, actionTitle: String, actionColor: Color, - diffEntries: List, - onDiffEntrySelected: (DiffEntry) -> Unit, - onDiffEntryOptionSelected: (DiffEntry) -> Unit, - onGenerateContextMenu: (DiffEntry) -> List, + statusEntries: List, + onDiffEntrySelected: (StatusEntry) -> Unit, + onDiffEntryOptionSelected: (StatusEntry) -> Unit, + onGenerateContextMenu: (StatusEntry) -> List, onAllAction: () -> Unit, allActionTitle: String, selectedEntryType: DiffEntryType?, @@ -477,24 +425,25 @@ private fun EntriesList( .fillMaxSize() .background(MaterialTheme.colors.background), ) { - itemsIndexed(diffEntries) { index, statusEntry -> - val diffEntry = statusEntry.diffEntry - val isEntrySelected = selectedEntryType?.diffEntry == diffEntry + itemsIndexed(statusEntries) { index, statusEntry -> + val isEntrySelected = selectedEntryType != null && + selectedEntryType is DiffEntryType.UncommitedDiff && // Added for smartcast + selectedEntryType.statusEntry == statusEntry FileEntry( statusEntry = statusEntry, isSelected = isEntrySelected, actionTitle = actionTitle, actionColor = actionColor, onClick = { - onDiffEntrySelected(diffEntry) + onDiffEntrySelected(statusEntry) }, onButtonClick = { - onDiffEntryOptionSelected(diffEntry) + onDiffEntryOptionSelected(statusEntry) }, onGenerateContextMenu = onGenerateContextMenu, ) - if (index < diffEntries.size - 1) { + if (index < statusEntries.size - 1) { Divider(modifier = Modifier.fillMaxWidth()) } } @@ -514,10 +463,9 @@ private fun FileEntry( actionColor: Color, onClick: () -> Unit, onButtonClick: () -> Unit, - onGenerateContextMenu: (DiffEntry) -> List, + onGenerateContextMenu: (StatusEntry) -> List, ) { var active by remember { mutableStateOf(false) } - val diffEntry = statusEntry.diffEntry val textColor: Color val secondaryTextColor: Color @@ -547,7 +495,7 @@ private fun FileEntry( ) { ContextMenuArea( items = { - onGenerateContextMenu(diffEntry) + onGenerateContextMenu(statusEntry) }, ) { Row( @@ -566,9 +514,9 @@ private fun FileEntry( tint = statusEntry.iconColor, ) - if(diffEntry.parentDirectoryPath.isNotEmpty()) { + if(statusEntry.parentDirectoryPath.isNotEmpty()) { Text( - text = diffEntry.parentDirectoryPath, + text = statusEntry.parentDirectoryPath, modifier = Modifier.weight(1f, fill = false), maxLines = 1, softWrap = false, @@ -578,7 +526,7 @@ private fun FileEntry( ) } Text( - text = diffEntry.fileName, + text = statusEntry.fileName, modifier = Modifier.weight(1f, fill = false), maxLines = 1, softWrap = false, diff --git a/src/main/kotlin/app/ui/context_menu/StagedEntriesContextMenu.kt b/src/main/kotlin/app/ui/context_menu/StagedEntriesContextMenu.kt index 899fd50..b9d2f10 100644 --- a/src/main/kotlin/app/ui/context_menu/StagedEntriesContextMenu.kt +++ b/src/main/kotlin/app/ui/context_menu/StagedEntriesContextMenu.kt @@ -2,15 +2,17 @@ package app.ui.context_menu import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ExperimentalFoundationApi +import app.git.StatusEntry +import app.git.StatusType import org.eclipse.jgit.diff.DiffEntry @OptIn(ExperimentalFoundationApi::class) fun stagedEntriesContextMenuItems( - diffEntry: DiffEntry, + diffEntry: StatusEntry, onReset: () -> Unit, ): List { return mutableListOf().apply { - if (diffEntry.changeType != DiffEntry.ChangeType.ADD) { + if (diffEntry.statusType != StatusType.ADDED) { add( ContextMenuItem( label = "Reset", diff --git a/src/main/kotlin/app/ui/context_menu/UnstagedEntriesContextMenu.kt b/src/main/kotlin/app/ui/context_menu/UnstagedEntriesContextMenu.kt index d49ed07..78fbb41 100644 --- a/src/main/kotlin/app/ui/context_menu/UnstagedEntriesContextMenu.kt +++ b/src/main/kotlin/app/ui/context_menu/UnstagedEntriesContextMenu.kt @@ -2,16 +2,18 @@ package app.ui.context_menu import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ExperimentalFoundationApi +import app.git.StatusEntry +import app.git.StatusType import org.eclipse.jgit.diff.DiffEntry @OptIn(ExperimentalFoundationApi::class) fun unstagedEntriesContextMenuItems( - diffEntry: DiffEntry, + statusEntry: StatusEntry, onReset: () -> Unit, onDelete: () -> Unit, ): List { return mutableListOf().apply { - if (diffEntry.changeType != DiffEntry.ChangeType.ADD) { + if (statusEntry.statusType != StatusType.ADDED) { add( ContextMenuItem( label = "Reset", @@ -20,7 +22,7 @@ fun unstagedEntriesContextMenuItems( ) } - if (diffEntry.changeType != DiffEntry.ChangeType.DELETE) { + if (statusEntry.statusType != StatusType.REMOVED) { add( ContextMenuItem( label = "Delete file", diff --git a/src/main/kotlin/app/ui/log/Log.kt b/src/main/kotlin/app/ui/log/Log.kt index 424261c..cc0395e 100644 --- a/src/main/kotlin/app/ui/log/Log.kt +++ b/src/main/kotlin/app/ui/log/Log.kt @@ -590,6 +590,14 @@ fun LogStatusSummary(statusSummary: StatusSummary, modifier: Modifier) { color = MaterialTheme.colors.deleteFile, ) } + + if (statusSummary.conflictingCount > 0) { + SummaryEntry( + count = statusSummary.conflictingCount, + icon = Icons.Default.Warning, + color = MaterialTheme.colors.conflictFile, + ) + } } } diff --git a/src/main/kotlin/app/viewmodels/DiffViewModel.kt b/src/main/kotlin/app/viewmodels/DiffViewModel.kt index 18550d5..2a360f9 100644 --- a/src/main/kotlin/app/viewmodels/DiffViewModel.kt +++ b/src/main/kotlin/app/viewmodels/DiffViewModel.kt @@ -1,6 +1,7 @@ package app.viewmodels import androidx.compose.foundation.lazy.LazyListState +import app.exceptions.MissingDiffEntryException import app.git.* import app.git.diff.DiffResult import app.git.diff.Hunk @@ -14,8 +15,7 @@ class DiffViewModel @Inject constructor( private val diffManager: DiffManager, private val statusManager: StatusManager, ) { - // TODO Maybe use a sealed class instead of a null to represent that a diff is not selected? - private val _diffResult = MutableStateFlow(null) + private val _diffResult = MutableStateFlow(ViewDiffResult.Loading) val diffResult: StateFlow = _diffResult val lazyListState = MutableStateFlow( @@ -25,18 +25,23 @@ class DiffViewModel @Inject constructor( ) ) + // TODO Cancel job if the user closed the diff view while loading fun updateDiff(diffEntryType: DiffEntryType) = tabState.runOperation( refreshType = RefreshType.NONE, ) { 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 (oldDiffEntryType != null && - (oldDiffEntryType.diffEntry.oldPath != diffEntryType.diffEntry.oldPath || - oldDiffEntryType.diffEntry.newPath != diffEntryType.diffEntry.newPath || - oldDiffEntryType::class != diffEntryType::class) + oldDiffEntryType is DiffEntryType.UncommitedDiff && diffEntryType is DiffEntryType.UncommitedDiff && + oldDiffEntryType.statusEntry.filePath != diffEntryType.statusEntry.filePath ) { lazyListState.value = LazyListState( 0, @@ -44,13 +49,14 @@ class DiffViewModel @Inject constructor( ) } - //TODO: Just a workaround when trying to diff binary files try { - val hunks = diffManager.diffFormat(git, diffEntryType) - _diffResult.value = ViewDiffResult(diffEntryType, hunks) + val diffFormat = diffManager.diffFormat(git, diffEntryType) + _diffResult.value = ViewDiffResult.Loaded(diffEntryType, diffFormat) } catch (ex: Exception) { - ex.printStackTrace() - _diffResult.value = ViewDiffResult(diffEntryType, DiffResult.Text(emptyList())) + if(ex is MissingDiffEntryException) { + _diffResult.value = ViewDiffResult.DiffNotFound + } else + ex.printStackTrace() } } @@ -67,4 +73,9 @@ class DiffViewModel @Inject constructor( } } -data class ViewDiffResult(val diffEntryType: DiffEntryType, val diffResult: DiffResult) \ No newline at end of file + +sealed interface ViewDiffResult { + object Loading: ViewDiffResult + object DiffNotFound: ViewDiffResult + data class Loaded(val diffEntryType: DiffEntryType, val diffResult: DiffResult): ViewDiffResult +} diff --git a/src/main/kotlin/app/viewmodels/LogViewModel.kt b/src/main/kotlin/app/viewmodels/LogViewModel.kt index 98e4f06..04a574f 100644 --- a/src/main/kotlin/app/viewmodels/LogViewModel.kt +++ b/src/main/kotlin/app/viewmodels/LogViewModel.kt @@ -56,18 +56,14 @@ class LogViewModel @Inject constructor( val statusSummary = statusManager.getStatusSummary( git = git, - currentBranch = currentBranch, - repositoryState = repositoryManager.getRepositoryState(git), ) - val hasUncommitedChanges = - statusSummary.addedCount + statusSummary.deletedCount + statusSummary.modifiedCount > 0 + val hasUncommitedChanges = statusSummary.total > 0 val log = logManager.loadLog(git, currentBranch, hasUncommitedChanges) _logStatus.value = LogStatus.Loaded(hasUncommitedChanges, log, currentBranch, statusSummary) // Remove search filter if the log has been updated - // TODO: Should we just update the search instead of closing it? _logSearchFilterResults.value = LogSearch.NotSearching } @@ -163,11 +159,9 @@ class LogViewModel @Inject constructor( val statsSummary = if (hasUncommitedChanges) { statusManager.getStatusSummary( git = git, - currentBranch = currentBranch, - repositoryState = repositoryManager.getRepositoryState(git), ) } else - StatusSummary(0, 0, 0) + StatusSummary(0, 0, 0, 0) val previousLogStatusValue = _logStatus.value diff --git a/src/main/kotlin/app/viewmodels/StatusViewModel.kt b/src/main/kotlin/app/viewmodels/StatusViewModel.kt index eed3378..7120e68 100644 --- a/src/main/kotlin/app/viewmodels/StatusViewModel.kt +++ b/src/main/kotlin/app/viewmodels/StatusViewModel.kt @@ -6,7 +6,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git -import org.eclipse.jgit.diff.DiffEntry import java.io.File import javax.inject.Inject @@ -27,16 +26,16 @@ class StatusViewModel @Inject constructor( private var lastUncommitedChangesState = false - fun stage(diffEntry: DiffEntry) = tabState.runOperation( + fun stage(statusEntry: StatusEntry) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { 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, ) { git -> - statusManager.unstage(git, diffEntry) + statusManager.unstage(git, statusEntry) } @@ -52,16 +51,16 @@ class StatusViewModel @Inject constructor( statusManager.stageAll(git) } - fun resetStaged(diffEntry: DiffEntry) = tabState.runOperation( + fun resetStaged(statusEntry: StatusEntry) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { 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, ) { git -> - statusManager.reset(git, diffEntry, staged = false) + statusManager.reset(git, statusEntry, staged = false) } private suspend fun loadStatus(git: Git) { @@ -69,10 +68,9 @@ class StatusViewModel @Inject constructor( try { _stageStatus.value = StageStatus.Loading - val repositoryState = repositoryManager.getRepositoryState(git) - val currentBranchRef = branchesManager.currentBranchRef(git) - val staged = statusManager.getStaged(git, currentBranchRef, repositoryState) - val unstaged = statusManager.getUnstaged(git, repositoryState) + val status = statusManager.getStatus(git) + val staged = statusManager.getStaged(status) + val unstaged = statusManager.getUnstaged(status) _stageStatus.value = StageStatus.Loaded(staged, unstaged) } catch (ex: Exception) { @@ -141,10 +139,10 @@ class StatusViewModel @Inject constructor( mergeManager.abortMerge(git) } - fun deleteFile(diffEntry: DiffEntry) = tabState.runOperation( + fun deleteFile(statusEntry: StatusEntry) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> - val path = diffEntry.newPath + val path = statusEntry.filePath val fileToDelete = File(git.repository.directory.parent, path)