Added option to stage by line individually
This commit is contained in:
parent
871264722a
commit
eafebd2221
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 = { _, _, _ -> },
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user