Added option to stage by line individually

This commit is contained in:
Abdelilah El Aissaoui 2022-10-09 23:44:50 +02:00
parent 871264722a
commit eafebd2221
4 changed files with 216 additions and 39 deletions

View File

@ -0,0 +1,76 @@
package com.jetpackduba.gitnuro.git.workspace
import com.jetpackduba.gitnuro.git.EntryContent
import com.jetpackduba.gitnuro.git.RawFileManager
import com.jetpackduba.gitnuro.git.diff.Hunk
import com.jetpackduba.gitnuro.git.diff.Line
import com.jetpackduba.gitnuro.git.diff.LineType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.diff.DiffEntry
import java.nio.ByteBuffer
import javax.inject.Inject
class StageHunkLineUseCase @Inject constructor(
private val rawFileManager: RawFileManager,
private val getLinesFromRawTextUseCase: GetLinesFromRawTextUseCase,
) {
suspend operator fun invoke(git: Git, diffEntry: DiffEntry, hunk: Hunk, lineToStage: Line) =
withContext(Dispatchers.IO) {
val repository = git.repository
val dirCache = repository.lockDirCache()
val dirCacheEditor = dirCache.editor()
var completedWithErrors = true
try {
val entryContent = rawFileManager.getRawContent(
repository = git.repository,
side = DiffEntry.Side.OLD,
entry = diffEntry,
oldTreeIterator = null,
newTreeIterator = null
)
if (entryContent !is EntryContent.Text)
return@withContext
val textLines = getLinesFromRawTextUseCase(entryContent.rawText).toMutableList()
when (lineToStage.lineType) {
LineType.ADDED -> {
val previousContextLine = hunk.lines
.takeWhile { it != lineToStage }
.lastOrNull { it.lineType == LineType.CONTEXT }
val startingIndex = previousContextLine?.oldLineNumber ?: -1
textLines.add(startingIndex + 1, lineToStage.text)
}
LineType.REMOVED -> {
textLines.removeAt(lineToStage.oldLineNumber)
}
else -> {}
}
val stagedFileText = textLines.joinToString("")
dirCacheEditor.add(
HunkEdit(
diffEntry.newPath,
repository,
ByteBuffer.wrap(stagedFileText.toByteArray())
)
)
dirCacheEditor.commit()
completedWithErrors = false
} finally {
if (completedWithErrors)
dirCache.unlock()
}
}
}

View File

@ -128,6 +128,7 @@ private fun HistoryContent(
scrollState = textScrollState,
onCommitSelected = onCommitSelected,
)
is HistoryState.Loading -> Box { }
}
}
@ -171,6 +172,7 @@ fun HistoryContentLoaded(
onUnstageHunk = { _, _ -> },
onStageHunk = { _, _ -> },
onResetHunk = { _, _ -> },
onActionTriggered = { _, _, _ -> }
)
}
@ -182,6 +184,7 @@ fun HistoryContentLoaded(
onUnstageHunk = { _, _ -> },
onStageHunk = { _, _ -> },
onResetHunk = { _, _ -> },
onActionTriggered = { _, _, _ -> },
)
}

View File

@ -2,12 +2,13 @@
package com.jetpackduba.gitnuro.ui.diff
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
@ -15,6 +16,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
@ -22,9 +24,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerIconDefaults
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.res.loadImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
@ -124,6 +124,12 @@ fun Diff(
},
onResetHunk = { entry, hunk ->
diffViewModel.resetHunk(entry, hunk)
},
onActionTriggered = { entry, hunk, line ->
if(diffEntryType is DiffEntryType.UnstagedDiff)
diffViewModel.stageHunkLine(entry, hunk, line)
else if(diffEntryType is DiffEntryType.StagedDiff)
diffViewModel.unstageHunkLine(entry, hunk, line)
}
)
@ -139,6 +145,12 @@ fun Diff(
},
onResetHunk = { entry, hunk ->
diffViewModel.resetHunk(entry, hunk)
},
onActionTriggered = { entry, hunk, line ->
if(diffEntryType is DiffEntryType.UnstagedDiff)
diffViewModel.stageHunkLine(entry, hunk, line)
else if(diffEntryType is DiffEntryType.StagedDiff)
diffViewModel.unstageHunkLine(entry, hunk, line)
}
)
@ -305,6 +317,7 @@ fun HunkUnifiedTextDiff(
diffResult: DiffResult.Text,
onUnstageHunk: (DiffEntry, Hunk) -> Unit,
onStageHunk: (DiffEntry, Hunk) -> Unit,
onActionTriggered: (DiffEntry, Hunk, Line) -> Unit,
onResetHunk: (DiffEntry, Hunk) -> Unit,
) {
val hunks = diffResult.hunks
@ -334,7 +347,18 @@ fun HunkUnifiedTextDiff(
val highestLineNumberLength = highestLineNumber.toString().count()
items(hunk.lines) { line ->
DiffLine(highestLineNumberLength, line)
DiffLine(
highestLineNumberLength,
line,
diffEntryType = diffEntryType,
onActionTriggered = {
onActionTriggered(
diffResult.diffEntry,
hunk,
line,
)
},
)
}
}
}
@ -350,8 +374,13 @@ fun HunkSplitTextDiff(
onUnstageHunk: (DiffEntry, Hunk) -> Unit,
onStageHunk: (DiffEntry, Hunk) -> Unit,
onResetHunk: (DiffEntry, Hunk) -> Unit,
onActionTriggered: (DiffEntry, Hunk, Line) -> Unit,
) {
val hunks = diffResult.hunks
/**
* Disables selection in one side when the other is being selected
*/
var selectableSide by remember { mutableStateOf(SelectableSide.BOTH) }
SelectionContainer {
@ -384,6 +413,10 @@ fun HunkSplitTextDiff(
oldLine = linesPair.first,
newLine = linesPair.second,
selectableSide = selectableSide,
diffEntryType = diffEntryType,
onActionTriggered = {line ->
onActionTriggered(diffResult.diffEntry, splitHunk.sourceHunk, line)
},
onChangeSelectableSide = { newSelectableSide ->
if (newSelectableSide != selectableSide) {
selectableSide = newSelectableSide
@ -410,7 +443,9 @@ fun SplitDiffLine(
oldLine: Line?,
newLine: Line?,
selectableSide: SelectableSide,
diffEntryType: DiffEntryType,
onChangeSelectableSide: (SelectableSide) -> Unit,
onActionTriggered: (Line) -> Unit,
) {
Row(
modifier = Modifier
@ -426,6 +461,8 @@ fun SplitDiffLine(
currentSelectableSide = selectableSide,
lineSelectableSide = SelectableSide.OLD,
onChangeSelectableSide = onChangeSelectableSide,
diffEntryType = diffEntryType,
onActionTriggered = { if (oldLine != null) onActionTriggered(oldLine) }
)
Box(
@ -444,6 +481,8 @@ fun SplitDiffLine(
currentSelectableSide = selectableSide,
lineSelectableSide = SelectableSide.NEW,
onChangeSelectableSide = onChangeSelectableSide,
diffEntryType = diffEntryType,
onActionTriggered = { if (newLine != null) onActionTriggered(newLine) }
)
}
@ -457,7 +496,9 @@ fun SplitDiffLineSide(
displayLineNumber: Int,
currentSelectableSide: SelectableSide,
lineSelectableSide: SelectableSide,
diffEntryType: DiffEntryType,
onChangeSelectableSide: (SelectableSide) -> Unit,
onActionTriggered: () -> Unit,
) {
Box(
modifier = modifier
@ -474,7 +515,13 @@ fun SplitDiffLineSide(
currentSelectableSide != lineSelectableSide &&
currentSelectableSide != SelectableSide.BOTH
) {
SplitDiffLine(highestLineNumberLength, line, displayLineNumber)
SplitDiffLine(
highestLineNumberLength = highestLineNumberLength,
line = line,
lineNumber = displayLineNumber,
diffEntryType = diffEntryType,
onActionTriggered = onActionTriggered,
)
}
}
}
@ -717,12 +764,15 @@ private fun PathOnlyDiffHeader(
fun DiffLine(
highestLineNumberLength: Int,
line: Line,
diffEntryType: DiffEntryType,
onActionTriggered: () -> Unit,
) {
val backgroundColor = when (line.lineType) {
LineType.ADDED -> MaterialTheme.colors.diffLineAdded
LineType.REMOVED -> MaterialTheme.colors.diffLineRemoved
LineType.CONTEXT -> MaterialTheme.colors.background
}
Row(
modifier = Modifier
.background(backgroundColor)
@ -750,7 +800,7 @@ fun DiffLine(
)
}
DiffLineText(line.text)
DiffLineText(line, diffEntryType, onActionTriggered = onActionTriggered)
}
}
@ -759,6 +809,8 @@ fun SplitDiffLine(
highestLineNumberLength: Int,
line: Line,
lineNumber: Int,
diffEntryType: DiffEntryType,
onActionTriggered: () -> Unit,
) {
val backgroundColor = when (line.lineType) {
LineType.ADDED -> MaterialTheme.colors.diffLineAdded
@ -777,13 +829,45 @@ fun SplitDiffLine(
)
}
DiffLineText(line.text)
DiffLineText(line, diffEntryType, onActionTriggered = onActionTriggered)
}
}
@Composable
fun DiffLineText(text: String) {
fun DiffLineText(line: Line, diffEntryType: DiffEntryType, onActionTriggered: () -> Unit) {
val text = line.text
val hoverInteraction = remember { MutableInteractionSource() }
val isHovered by hoverInteraction.collectIsHoveredAsState()
Box(modifier = Modifier.hoverable(hoverInteraction)) {
if (isHovered && diffEntryType is DiffEntryType.UncommitedDiff && line.lineType != LineType.CONTEXT) {
val color: Color = if (diffEntryType is DiffEntryType.StagedDiff) {
MaterialTheme.colors.error
} else {
MaterialTheme.colors.primary
}
val iconName = remember(diffEntryType) {
if (diffEntryType is DiffEntryType.StagedDiff) {
"remove.svg"
} else {
"add.svg"
}
}
Icon(
painterResource(iconName),
contentDescription = null,
tint = Color.White,
modifier = Modifier
.fastClickable { onActionTriggered() }
.size(14.dp)
.clip(RoundedCornerShape(2.dp))
.background(color),
)
}
Row {
Text(
text = text.replace(
@ -791,7 +875,7 @@ fun DiffLineText(text: String) {
" "
).removeLineDelimiters(),
modifier = Modifier
.padding(start = 8.dp)
.padding(start = 16.dp)
.fillMaxSize(),
fontFamily = FontFamily.Monospace,
style = MaterialTheme.typography.body2,
@ -811,7 +895,7 @@ fun DiffLineText(text: String) {
color = MaterialTheme.colors.onBackground,
)
}
}
}
}

View File

@ -4,15 +4,12 @@ package com.jetpackduba.gitnuro.viewmodels
import androidx.compose.foundation.lazy.LazyListState
import com.jetpackduba.gitnuro.exceptions.MissingDiffEntryException
import com.jetpackduba.gitnuro.extensions.delayedStateChange
import com.jetpackduba.gitnuro.git.diff.DiffResult
import com.jetpackduba.gitnuro.git.diff.FormatDiffUseCase
import com.jetpackduba.gitnuro.git.diff.Hunk
import com.jetpackduba.gitnuro.preferences.AppSettings
import com.jetpackduba.gitnuro.git.diff.GenerateSplitHunkFromDiffResultUseCase
import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.diff.*
import com.jetpackduba.gitnuro.git.workspace.*
import com.jetpackduba.gitnuro.preferences.AppSettings
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -27,6 +24,7 @@ class DiffViewModel @Inject constructor(
private val formatDiffUseCase: FormatDiffUseCase,
private val stageHunkUseCase: StageHunkUseCase,
private val unstageHunkUseCase: UnstageHunkUseCase,
private val stageHunkLineUseCase: StageHunkLineUseCase,
private val resetHunkUseCase: ResetHunkUseCase,
private val stageEntryUseCase: StageEntryUseCase,
private val unstageEntryUseCase: UnstageEntryUseCase,
@ -140,12 +138,14 @@ class DiffViewModel @Inject constructor(
fun stageFile(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES,
showError = true,
) { git ->
stageEntryUseCase(git, statusEntry)
}
fun unstageFile(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES,
showError = true,
) { git ->
unstageEntryUseCase(git, statusEntry)
}
@ -157,6 +157,20 @@ class DiffViewModel @Inject constructor(
fun changeTextDiffType(newDiffType: TextDiffType) {
settings.textDiffType = newDiffType
}
fun stageHunkLine(entry: DiffEntry, hunk: Hunk, line: Line) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES,
showError = true,
) { git ->
stageHunkLineUseCase(git, entry, hunk, line)
}
fun unstageHunkLine(entry: DiffEntry, hunk: Hunk, line: Line) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES,
showError = true,
) { git ->
throw NotImplementedError()
}
}
enum class TextDiffType(val value: Int) {