From 5f2180f1a30e489a5e962c13267b37135e386503 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Thu, 14 Sep 2023 01:05:06 +0200 Subject: [PATCH] Clicking on a submodule in (un)commited changes shows an informative screen --- .../jetpackduba/gitnuro/git/RawFileManager.kt | 2 + .../gitnuro/git/diff/DiffResult.kt | 6 ++ .../gitnuro/git/diff/FormatDiffUseCase.kt | 70 ++++++++----- .../gitnuro/ui/UncommitedChanges.kt | 3 - .../ui/context_menu/SubmoduleContextMenu.kt | 8 +- .../com/jetpackduba/gitnuro/ui/diff/Diff.kt | 98 +++++++++++++------ .../gitnuro/viewmodels/DiffViewModel.kt | 6 ++ 7 files changed, 130 insertions(+), 63 deletions(-) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt index 599c56d..abe8236 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt @@ -43,6 +43,7 @@ class RawFileManager @Inject constructor( newTreeIterator: AbstractTreeIterator?, ): EntryContent { if (entry.getMode(side) === FileMode.MISSING) return EntryContent.Missing + if (entry.getMode(side).objectType == Constants.OBJ_COMMIT) return EntryContent.Submodule if (entry.getMode(side).objectType != Constants.OBJ_BLOB) return EntryContent.InvalidObjectBlob val reader: ObjectReader = repository.newObjectReader() @@ -111,6 +112,7 @@ sealed class EntryContent { object Missing : EntryContent() object InvalidObjectBlob : EntryContent() data class Text(val rawText: RawText) : EntryContent() + object Submodule : EntryContent() sealed class BinaryContent : EntryContent() data class ImageBinary(val imagePath: String, val contentType: String) : BinaryContent() object Binary : BinaryContent() diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/DiffResult.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/DiffResult.kt index dd03935..7faeb0c 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/DiffResult.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/DiffResult.kt @@ -2,6 +2,7 @@ package com.jetpackduba.gitnuro.git.diff import com.jetpackduba.gitnuro.git.EntryContent import org.eclipse.jgit.diff.DiffEntry +import org.eclipse.jgit.submodule.SubmoduleStatus sealed class DiffResult( val diffEntry: DiffEntry, @@ -21,4 +22,9 @@ sealed class DiffResult( val oldBinaryContent: EntryContent, val newBinaryContent: EntryContent, ) : DiffResult(diffEntry) + + class Submodule( + diffEntry: DiffEntry, + val submoduleStatus: SubmoduleStatus?, + ) : DiffResult(diffEntry) } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/FormatDiffUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/FormatDiffUseCase.kt index f47b0fe..609397e 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/FormatDiffUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/diff/FormatDiffUseCase.kt @@ -1,23 +1,28 @@ package com.jetpackduba.gitnuro.git.diff +import com.jetpackduba.gitnuro.extensions.filePath import com.jetpackduba.gitnuro.git.DiffEntryType import com.jetpackduba.gitnuro.git.EntryContent +import com.jetpackduba.gitnuro.git.submodules.GetSubmodulesUseCase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffFormatter import org.eclipse.jgit.dircache.DirCacheIterator +import org.eclipse.jgit.submodule.SubmoduleStatus +import org.eclipse.jgit.submodule.SubmoduleStatusType import org.eclipse.jgit.treewalk.FileTreeIterator import java.io.ByteArrayOutputStream import java.io.InvalidObjectException import javax.inject.Inject class FormatDiffUseCase @Inject constructor( - private val getDiffEntryForUncommitedDiffUseCase: GetDiffEntryForUncommitedDiffUseCase, - private val canGenerateTextDiffUseCase: CanGenerateTextDiffUseCase, - private val getDiffContentUseCase: GetDiffContentUseCase, private val formatHunksUseCase: FormatHunksUseCase, + private val getDiffContentUseCase: GetDiffContentUseCase, + private val canGenerateTextDiffUseCase: CanGenerateTextDiffUseCase, + private val getDiffEntryForUncommitedDiffUseCase: GetDiffEntryForUncommitedDiffUseCase, + private val getSubmodulesUseCase: GetSubmodulesUseCase, ) { suspend operator fun invoke( git: Git, @@ -27,6 +32,7 @@ class FormatDiffUseCase @Inject constructor( val byteArrayOutputStream = ByteArrayOutputStream() val repository = git.repository val diffEntry: DiffEntry + val submodules = getSubmodulesUseCase(git) DiffFormatter(byteArrayOutputStream).use { formatter -> formatter.setRepository(repository) @@ -51,37 +57,49 @@ class FormatDiffUseCase @Inject constructor( formatter.flush() } - val oldTree: DirCacheIterator? - val newTree: FileTreeIterator? + var diffResult: DiffResult + val submoduleStatus = submodules[diffEntry.filePath] - if (diffEntryType is DiffEntryType.UnstagedDiff) { - oldTree = DirCacheIterator(repository.readDirCache()) - newTree = FileTreeIterator(repository) + if (submoduleStatus != null) { + diffResult = DiffResult.Submodule(diffEntry, submoduleStatus) } else { - oldTree = null - newTree = null - } + val oldTree: DirCacheIterator? + val newTree: FileTreeIterator? - val diffContent = getDiffContentUseCase(repository, diffEntry, oldTree, newTree) - val fileHeader = diffContent.fileHeader + if (diffEntryType is DiffEntryType.UnstagedDiff) { + oldTree = DirCacheIterator(repository.readDirCache()) + newTree = FileTreeIterator(repository) + } else { + oldTree = null + newTree = null + } - val rawOld = diffContent.rawOld - val rawNew = diffContent.rawNew + val diffContent = getDiffContentUseCase(repository, diffEntry, oldTree, newTree) + val fileHeader = diffContent.fileHeader - if (rawOld == EntryContent.InvalidObjectBlob || rawNew == EntryContent.InvalidObjectBlob) - throw InvalidObjectException("Invalid object in diff format") + val rawOld = diffContent.rawOld + val rawNew = diffContent.rawNew - var diffResult: DiffResult = DiffResult.Text(diffEntry, emptyList()) + if (rawOld == EntryContent.InvalidObjectBlob || rawNew == EntryContent.InvalidObjectBlob) { + throw InvalidObjectException("Invalid object in diff format") + } else if (rawOld == EntryContent.Submodule || rawNew == EntryContent.Submodule) { + diffResult = DiffResult.Submodule(diffEntry, null) + } else { + diffResult = DiffResult.Text(diffEntry, emptyList()) - // If we can, generate text diff (if one of the files has never been a binary file) - val hasGeneratedTextDiff = canGenerateTextDiffUseCase(rawOld, rawNew) { oldRawText, newRawText -> - diffResult = - DiffResult.Text(diffEntry, formatHunksUseCase(fileHeader, oldRawText, newRawText, isDisplayFullFile)) + // If we can, generate text diff (if one of the files has never been a binary file) + val hasGeneratedTextDiff = canGenerateTextDiffUseCase(rawOld, rawNew) { oldRawText, newRawText -> + diffResult = + DiffResult.Text( + diffEntry, + formatHunksUseCase(fileHeader, oldRawText, newRawText, isDisplayFullFile) + ) + } - } - - if (!hasGeneratedTextDiff) { - diffResult = DiffResult.NonText(diffEntry, rawOld, rawNew) + if (!hasGeneratedTextDiff) { + diffResult = DiffResult.NonText(diffEntry, rawOld, rawNew) + } + } } return@withContext diffResult diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt index 28f548c..698d38c 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt @@ -176,9 +176,6 @@ fun UncommitedChanges( ) } - // TODO After moving to compose 1.5.0 (beta and RC), the right click event stops working randomly just on - // unstaged changes. It could be related to the changes to the Popup API and the custom implementation - // that Gitnuro uses val unstagedView: @Composable () -> Unit = { EntriesList( modifier = Modifier diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/SubmoduleContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/SubmoduleContextMenu.kt index c3ae9fb..5455e9a 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/SubmoduleContextMenu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/SubmoduleContextMenu.kt @@ -56,9 +56,11 @@ fun submoduleContextMenuItems( // ) } - add( - ContextMenuElement.ContextSeparator, - ) + if(isNotEmpty()) { + add( + ContextMenuElement.ContextSeparator, + ) + } add( ContextMenuElement.ContextTextEntry( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt index 406b8fb..1a0f0a7 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.platform.PlatformLocalization import androidx.compose.ui.res.loadImageBitmap import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -53,6 +54,7 @@ import com.jetpackduba.gitnuro.git.workspace.StatusType import com.jetpackduba.gitnuro.keybindings.KeybindingOption import com.jetpackduba.gitnuro.keybindings.matchesBinding import com.jetpackduba.gitnuro.theme.* +import com.jetpackduba.gitnuro.ui.components.PrimaryButton import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn import com.jetpackduba.gitnuro.ui.components.SecondaryButton import com.jetpackduba.gitnuro.ui.components.Tooltip @@ -65,6 +67,7 @@ import com.jetpackduba.gitnuro.viewmodels.ViewDiffResult import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.eclipse.jgit.diff.DiffEntry +import org.eclipse.jgit.submodule.SubmoduleStatusType import org.jetbrains.compose.animatedimage.Blank import org.jetbrains.compose.animatedimage.animate import org.jetbrains.compose.animatedimage.loadAnimatedImage @@ -118,6 +121,7 @@ fun Diff( diffEntry = diffEntry, onCloseDiffView = onCloseDiffView, diffType = diffType, + isTextDiff = diffResult is DiffResult.Text, isDisplayFullFile = isDisplayFullFile, onStageFile = { diffViewModel.stageFile(it) }, onUnstageFile = { diffViewModel.unstageFile(it) }, @@ -181,6 +185,13 @@ fun Diff( diffResult, onOpenFileWithExternalApp = { path -> diffViewModel.openFileWithExternalApp(path) }) } + + is DiffResult.Submodule -> { + SubmoduleDiff( + diffResult, + onOpenSubmodule = { diffViewModel.openSubmodule(diffResult.diffEntry.filePath) } + ) + } } } @@ -259,6 +270,60 @@ fun NonTextDiff(diffResult: DiffResult.NonText, onOpenFileWithExternalApp: (Stri } } +@Composable +fun SubmoduleDiff(diffResult: DiffResult.Submodule, onOpenSubmodule: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Submodule ${diffResult.diffEntry.filePath}", + style = MaterialTheme.typography.h4, + modifier = Modifier.padding(bottom = 8.dp), + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colors.onBackground, + ) + + SelectionContainer { + Column { + Text( + AnnotatedString( + "Old ID: ", + SpanStyle(fontWeight = FontWeight.SemiBold) + ) + AnnotatedString(diffResult.diffEntry.oldId.name()), + color = MaterialTheme.colors.onBackground, + ) + Text( + AnnotatedString( + "New ID: ", + SpanStyle(fontWeight = FontWeight.SemiBold) + ) + AnnotatedString(diffResult.diffEntry.newId.name()), + color = MaterialTheme.colors.onBackground, + ) + } + } + + val submoduleStatus = diffResult.submoduleStatus + + if ( + submoduleStatus != null && + listOf( + SubmoduleStatusType.INITIALIZED, + SubmoduleStatusType.REV_CHECKED_OUT + ).contains(submoduleStatus.type) && + submoduleStatus.indexId == diffResult.diffEntry.newId?.toObjectId() + ) { + PrimaryButton( + modifier = Modifier.padding(top = 8.dp), + text = "Open submodule", + onClick = onOpenSubmodule, + ) + } + } + +} + @Composable fun SideTitle(text: String) { Text( @@ -778,6 +843,7 @@ private fun DiffHeader( diffEntry: DiffEntry, diffType: TextDiffType, isDisplayFullFile: Boolean, + isTextDiff: Boolean, onCloseDiffView: () -> Unit, onStageFile: (StatusEntry) -> Unit, onUnstageFile: (StatusEntry) -> Unit, @@ -837,7 +903,7 @@ private fun DiffHeader( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - if (diffEntryType.statusType != StatusType.ADDED && diffEntryType.statusType != StatusType.REMOVED) { + if (diffEntryType.statusType != StatusType.ADDED && diffEntryType.statusType != StatusType.REMOVED && isTextDiff) { DiffTypeButtons( diffType = diffType, isDisplayFullFile = isDisplayFullFile, @@ -948,36 +1014,6 @@ fun DiffTypeButtons( onClick = { onChangeDiffType(TextDiffType.SPLIT) }, ) } -// -// Text( -// "Unified", -// color = MaterialTheme.colors.onBackground, -// style = MaterialTheme.typography.caption, -// ) -// -// Switch( -// checked = diffType == TextDiffType.SPLIT, -// onCheckedChange = { checked -> -// val newType = if (checked) -// TextDiffType.SPLIT -// else -// TextDiffType.UNIFIED -// -// onChangeDiffType(newType) -// }, -// colors = SwitchDefaults.colors( -// uncheckedThumbColor = MaterialTheme.colors.secondaryVariant, -// uncheckedTrackColor = MaterialTheme.colors.secondaryVariant, -// uncheckedTrackAlpha = 0.54f -// ) -// ) -// -// Text( -// "Split", -// color = MaterialTheme.colors.onBackground, -//// modifier = Modifier.padding(horizontal = 4.dp), -// style = MaterialTheme.typography.caption, -// ) } } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt index c33e3b8..1ad2c82 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt @@ -11,6 +11,7 @@ import com.jetpackduba.gitnuro.git.diff.* import com.jetpackduba.gitnuro.git.workspace.* import com.jetpackduba.gitnuro.preferences.AppSettings import com.jetpackduba.gitnuro.system.OpenFileInExternalAppUseCase +import com.jetpackduba.gitnuro.ui.TabsManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -36,6 +37,7 @@ class DiffViewModel @Inject constructor( private val settings: AppSettings, private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase, private val discardUnstagedHunkLineUseCase: DiscardUnstagedHunkLineUseCase, + private val tabsManager: TabsManager, tabScope: CoroutineScope, ) { private val _diffResult = MutableStateFlow(ViewDiffResult.Loading("")) @@ -218,6 +220,10 @@ class DiffViewModel @Inject constructor( ) { git -> discardUnstagedHunkLineUseCase(git, entry, hunk, line) } + + fun openSubmodule(path: String) = tabState.runOperation(refreshType = RefreshType.NONE) { git -> + tabsManager.addNewTabFromPath("${git.repository.workTree}/$path", true) + } } enum class TextDiffType(val value: Int) {