Clicking on a submodule in (un)commited changes shows an informative screen

This commit is contained in:
Abdelilah El Aissaoui 2023-09-14 01:05:06 +02:00
parent f8a6884098
commit 5f2180f1a3
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
7 changed files with 130 additions and 63 deletions

View File

@ -43,6 +43,7 @@ class RawFileManager @Inject constructor(
newTreeIterator: AbstractTreeIterator?, newTreeIterator: AbstractTreeIterator?,
): EntryContent { ): EntryContent {
if (entry.getMode(side) === FileMode.MISSING) return EntryContent.Missing 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 if (entry.getMode(side).objectType != Constants.OBJ_BLOB) return EntryContent.InvalidObjectBlob
val reader: ObjectReader = repository.newObjectReader() val reader: ObjectReader = repository.newObjectReader()
@ -111,6 +112,7 @@ sealed class EntryContent {
object Missing : EntryContent() object Missing : EntryContent()
object InvalidObjectBlob : EntryContent() object InvalidObjectBlob : EntryContent()
data class Text(val rawText: RawText) : EntryContent() data class Text(val rawText: RawText) : EntryContent()
object Submodule : EntryContent()
sealed class BinaryContent : EntryContent() sealed class BinaryContent : EntryContent()
data class ImageBinary(val imagePath: String, val contentType: String) : BinaryContent() data class ImageBinary(val imagePath: String, val contentType: String) : BinaryContent()
object Binary : BinaryContent() object Binary : BinaryContent()

View File

@ -2,6 +2,7 @@ package com.jetpackduba.gitnuro.git.diff
import com.jetpackduba.gitnuro.git.EntryContent import com.jetpackduba.gitnuro.git.EntryContent
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.submodule.SubmoduleStatus
sealed class DiffResult( sealed class DiffResult(
val diffEntry: DiffEntry, val diffEntry: DiffEntry,
@ -21,4 +22,9 @@ sealed class DiffResult(
val oldBinaryContent: EntryContent, val oldBinaryContent: EntryContent,
val newBinaryContent: EntryContent, val newBinaryContent: EntryContent,
) : DiffResult(diffEntry) ) : DiffResult(diffEntry)
class Submodule(
diffEntry: DiffEntry,
val submoduleStatus: SubmoduleStatus?,
) : DiffResult(diffEntry)
} }

View File

@ -1,23 +1,28 @@
package com.jetpackduba.gitnuro.git.diff package com.jetpackduba.gitnuro.git.diff
import com.jetpackduba.gitnuro.extensions.filePath
import com.jetpackduba.gitnuro.git.DiffEntryType import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.git.EntryContent import com.jetpackduba.gitnuro.git.EntryContent
import com.jetpackduba.gitnuro.git.submodules.GetSubmodulesUseCase
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
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.diff.DiffFormatter import org.eclipse.jgit.diff.DiffFormatter
import org.eclipse.jgit.dircache.DirCacheIterator 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 org.eclipse.jgit.treewalk.FileTreeIterator
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InvalidObjectException import java.io.InvalidObjectException
import javax.inject.Inject import javax.inject.Inject
class FormatDiffUseCase @Inject constructor( class FormatDiffUseCase @Inject constructor(
private val getDiffEntryForUncommitedDiffUseCase: GetDiffEntryForUncommitedDiffUseCase,
private val canGenerateTextDiffUseCase: CanGenerateTextDiffUseCase,
private val getDiffContentUseCase: GetDiffContentUseCase,
private val formatHunksUseCase: FormatHunksUseCase, private val formatHunksUseCase: FormatHunksUseCase,
private val getDiffContentUseCase: GetDiffContentUseCase,
private val canGenerateTextDiffUseCase: CanGenerateTextDiffUseCase,
private val getDiffEntryForUncommitedDiffUseCase: GetDiffEntryForUncommitedDiffUseCase,
private val getSubmodulesUseCase: GetSubmodulesUseCase,
) { ) {
suspend operator fun invoke( suspend operator fun invoke(
git: Git, git: Git,
@ -27,6 +32,7 @@ class FormatDiffUseCase @Inject constructor(
val byteArrayOutputStream = ByteArrayOutputStream() val byteArrayOutputStream = ByteArrayOutputStream()
val repository = git.repository val repository = git.repository
val diffEntry: DiffEntry val diffEntry: DiffEntry
val submodules = getSubmodulesUseCase(git)
DiffFormatter(byteArrayOutputStream).use { formatter -> DiffFormatter(byteArrayOutputStream).use { formatter ->
formatter.setRepository(repository) formatter.setRepository(repository)
@ -51,37 +57,49 @@ class FormatDiffUseCase @Inject constructor(
formatter.flush() formatter.flush()
} }
val oldTree: DirCacheIterator? var diffResult: DiffResult
val newTree: FileTreeIterator? val submoduleStatus = submodules[diffEntry.filePath]
if (diffEntryType is DiffEntryType.UnstagedDiff) { if (submoduleStatus != null) {
oldTree = DirCacheIterator(repository.readDirCache()) diffResult = DiffResult.Submodule(diffEntry, submoduleStatus)
newTree = FileTreeIterator(repository)
} else { } else {
oldTree = null val oldTree: DirCacheIterator?
newTree = null val newTree: FileTreeIterator?
}
val diffContent = getDiffContentUseCase(repository, diffEntry, oldTree, newTree) if (diffEntryType is DiffEntryType.UnstagedDiff) {
val fileHeader = diffContent.fileHeader oldTree = DirCacheIterator(repository.readDirCache())
newTree = FileTreeIterator(repository)
} else {
oldTree = null
newTree = null
}
val rawOld = diffContent.rawOld val diffContent = getDiffContentUseCase(repository, diffEntry, oldTree, newTree)
val rawNew = diffContent.rawNew val fileHeader = diffContent.fileHeader
if (rawOld == EntryContent.InvalidObjectBlob || rawNew == EntryContent.InvalidObjectBlob) val rawOld = diffContent.rawOld
throw InvalidObjectException("Invalid object in diff format") 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) // If we can, generate text diff (if one of the files has never been a binary file)
val hasGeneratedTextDiff = canGenerateTextDiffUseCase(rawOld, rawNew) { oldRawText, newRawText -> val hasGeneratedTextDiff = canGenerateTextDiffUseCase(rawOld, rawNew) { oldRawText, newRawText ->
diffResult = diffResult =
DiffResult.Text(diffEntry, formatHunksUseCase(fileHeader, oldRawText, newRawText, isDisplayFullFile)) 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 return@withContext diffResult

View File

@ -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 = { val unstagedView: @Composable () -> Unit = {
EntriesList( EntriesList(
modifier = Modifier modifier = Modifier

View File

@ -56,9 +56,11 @@ fun submoduleContextMenuItems(
// ) // )
} }
add( if(isNotEmpty()) {
ContextMenuElement.ContextSeparator, add(
) ContextMenuElement.ContextSeparator,
)
}
add( add(
ContextMenuElement.ContextTextEntry( ContextMenuElement.ContextTextEntry(

View File

@ -35,6 +35,7 @@ import androidx.compose.ui.platform.PlatformLocalization
import androidx.compose.ui.res.loadImageBitmap import androidx.compose.ui.res.loadImageBitmap
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp 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.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.theme.* 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.ScrollableLazyColumn
import com.jetpackduba.gitnuro.ui.components.SecondaryButton import com.jetpackduba.gitnuro.ui.components.SecondaryButton
import com.jetpackduba.gitnuro.ui.components.Tooltip import com.jetpackduba.gitnuro.ui.components.Tooltip
@ -65,6 +67,7 @@ import com.jetpackduba.gitnuro.viewmodels.ViewDiffResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.submodule.SubmoduleStatusType
import org.jetbrains.compose.animatedimage.Blank import org.jetbrains.compose.animatedimage.Blank
import org.jetbrains.compose.animatedimage.animate import org.jetbrains.compose.animatedimage.animate
import org.jetbrains.compose.animatedimage.loadAnimatedImage import org.jetbrains.compose.animatedimage.loadAnimatedImage
@ -118,6 +121,7 @@ fun Diff(
diffEntry = diffEntry, diffEntry = diffEntry,
onCloseDiffView = onCloseDiffView, onCloseDiffView = onCloseDiffView,
diffType = diffType, diffType = diffType,
isTextDiff = diffResult is DiffResult.Text,
isDisplayFullFile = isDisplayFullFile, isDisplayFullFile = isDisplayFullFile,
onStageFile = { diffViewModel.stageFile(it) }, onStageFile = { diffViewModel.stageFile(it) },
onUnstageFile = { diffViewModel.unstageFile(it) }, onUnstageFile = { diffViewModel.unstageFile(it) },
@ -181,6 +185,13 @@ fun Diff(
diffResult, diffResult,
onOpenFileWithExternalApp = { path -> diffViewModel.openFileWithExternalApp(path) }) 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 @Composable
fun SideTitle(text: String) { fun SideTitle(text: String) {
Text( Text(
@ -778,6 +843,7 @@ private fun DiffHeader(
diffEntry: DiffEntry, diffEntry: DiffEntry,
diffType: TextDiffType, diffType: TextDiffType,
isDisplayFullFile: Boolean, isDisplayFullFile: Boolean,
isTextDiff: Boolean,
onCloseDiffView: () -> Unit, onCloseDiffView: () -> Unit,
onStageFile: (StatusEntry) -> Unit, onStageFile: (StatusEntry) -> Unit,
onUnstageFile: (StatusEntry) -> Unit, onUnstageFile: (StatusEntry) -> Unit,
@ -837,7 +903,7 @@ private fun DiffHeader(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp) 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( DiffTypeButtons(
diffType = diffType, diffType = diffType,
isDisplayFullFile = isDisplayFullFile, isDisplayFullFile = isDisplayFullFile,
@ -948,36 +1014,6 @@ fun DiffTypeButtons(
onClick = { onChangeDiffType(TextDiffType.SPLIT) }, 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,
// )
} }
} }

View File

@ -11,6 +11,7 @@ import com.jetpackduba.gitnuro.git.diff.*
import com.jetpackduba.gitnuro.git.workspace.* import com.jetpackduba.gitnuro.git.workspace.*
import com.jetpackduba.gitnuro.preferences.AppSettings import com.jetpackduba.gitnuro.preferences.AppSettings
import com.jetpackduba.gitnuro.system.OpenFileInExternalAppUseCase import com.jetpackduba.gitnuro.system.OpenFileInExternalAppUseCase
import com.jetpackduba.gitnuro.ui.TabsManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -36,6 +37,7 @@ class DiffViewModel @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase, private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase,
private val discardUnstagedHunkLineUseCase: DiscardUnstagedHunkLineUseCase, private val discardUnstagedHunkLineUseCase: DiscardUnstagedHunkLineUseCase,
private val tabsManager: TabsManager,
tabScope: CoroutineScope, tabScope: CoroutineScope,
) { ) {
private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading("")) private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading(""))
@ -218,6 +220,10 @@ class DiffViewModel @Inject constructor(
) { git -> ) { git ->
discardUnstagedHunkLineUseCase(git, entry, hunk, line) 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) { enum class TextDiffType(val value: Int) {