Implemented stage/unstage by hunks
This commit is contained in:
parent
86fd61d0d7
commit
a8ed01784d
@ -28,4 +28,9 @@ val String.dirPath: String
|
||||
parts.joinToString("/")
|
||||
} else
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
val String.withoutLineEnding: String
|
||||
get() = this
|
||||
.removeSuffix("\n")
|
||||
.removeSuffix("\r\n")
|
@ -173,6 +173,18 @@ class GitManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = managerScope.launch {
|
||||
runOperation {
|
||||
statusManager.stageHunk(safeGit, diffEntry, hunk)
|
||||
}
|
||||
}
|
||||
|
||||
fun unstageHunk(diffEntry: DiffEntry, hunk: Hunk) = managerScope.launch {
|
||||
runOperation {
|
||||
statusManager.unstageHunk(safeGit, diffEntry, hunk)
|
||||
}
|
||||
}
|
||||
|
||||
fun unstage(diffEntry: DiffEntry) = managerScope.launch {
|
||||
runOperation {
|
||||
statusManager.unstage(safeGit, diffEntry)
|
||||
|
@ -1,7 +1,12 @@
|
||||
package app.git
|
||||
|
||||
import app.di.RawFileManagerFactory
|
||||
import app.extensions.filePath
|
||||
import app.extensions.hasUntrackedChanges
|
||||
import app.extensions.isMerging
|
||||
import app.extensions.withoutLineEnding
|
||||
import app.git.diff.Hunk
|
||||
import app.git.diff.LineType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -9,12 +14,21 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.diff.DiffEntry
|
||||
import org.eclipse.jgit.lib.RepositoryState
|
||||
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.treewalk.EmptyTreeIterator
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class StatusManager @Inject constructor(
|
||||
private val branchesManager: BranchesManager,
|
||||
private val rawFileManagerFactory: RawFileManagerFactory,
|
||||
) {
|
||||
private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf()))
|
||||
val stageStatus: StateFlow<StageStatus> = _stageStatus
|
||||
@ -53,7 +67,7 @@ class StatusManager @Inject constructor(
|
||||
val currentBranch = branchesManager.currentBranchRef(git)
|
||||
val repositoryState = git.repository.repositoryState
|
||||
val staged = git.diff().apply {
|
||||
if(currentBranch == null && repositoryState != RepositoryState.MERGING && !repositoryState.isRebasing )
|
||||
if (currentBranch == null && !repositoryState.isMerging && !repositoryState.isRebasing)
|
||||
setOldTree(EmptyTreeIterator()) // Required if the repository is empty
|
||||
|
||||
setCached(true)
|
||||
@ -64,7 +78,7 @@ class StatusManager @Inject constructor(
|
||||
.map {
|
||||
val entries = it.value
|
||||
|
||||
if(entries.count() > 1)
|
||||
if (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing))
|
||||
entries.filter { entry -> entry.oldPath != "/dev/null" }
|
||||
else
|
||||
entries
|
||||
@ -79,7 +93,7 @@ class StatusManager @Inject constructor(
|
||||
.map {
|
||||
val entries = it.value
|
||||
|
||||
if(entries.count() > 1)
|
||||
if (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing))
|
||||
entries.filter { entry -> entry.newPath != "/dev/null" }
|
||||
else
|
||||
entries
|
||||
@ -108,12 +122,121 @@ class StatusManager @Inject constructor(
|
||||
loadStatus(git)
|
||||
}
|
||||
|
||||
// suspend fun stageHunk(git: Git) {
|
||||
//// val repository = git.repository
|
||||
//// val objectInserter = repository.newObjectInserter()
|
||||
//
|
||||
//// objectInserter.insert(Constants.OBJ_BLOB,)
|
||||
// }
|
||||
suspend fun stageHunk(git: Git, diffEntry: DiffEntry, hunk: Hunk) = withContext(Dispatchers.IO) {
|
||||
val repository = git.repository
|
||||
val dirCache = repository.lockDirCache()
|
||||
val dirCacheEditor = dirCache.editor()
|
||||
var completedWithErrors = true
|
||||
|
||||
try {
|
||||
val rawFileManager = rawFileManagerFactory.create(git.repository)
|
||||
val rawFile = rawFileManager.getRawContent(DiffEntry.Side.OLD, diffEntry)
|
||||
val textLines = getTextLines(rawFile).toMutableList()
|
||||
|
||||
val hunkLines = hunk.lines.filter { it.lineType != LineType.CONTEXT }
|
||||
|
||||
var linesAdded = 0
|
||||
for (line in hunkLines) {
|
||||
when (line.lineType) {
|
||||
LineType.ADDED -> {
|
||||
textLines.add(line.oldLineNumber + linesAdded, line.text.withoutLineEnding)
|
||||
linesAdded++
|
||||
}
|
||||
LineType.REMOVED -> {
|
||||
textLines.removeAt(line.oldLineNumber + linesAdded)
|
||||
linesAdded--
|
||||
}
|
||||
else -> throw NotImplementedError("Line type not implemented for stage hunk")
|
||||
}
|
||||
}
|
||||
|
||||
val stagedFileText = textLines.joinToString(rawFile.lineDelimiter)
|
||||
dirCacheEditor.add(HunkEdit(diffEntry.newPath, repository, ByteBuffer.wrap(stagedFileText.toByteArray())))
|
||||
dirCacheEditor.commit()
|
||||
|
||||
completedWithErrors = false
|
||||
|
||||
loadStatus(git)
|
||||
} finally {
|
||||
if (completedWithErrors)
|
||||
dirCache.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unstageHunk(git: Git, diffEntry: DiffEntry, hunk: Hunk) = withContext(Dispatchers.IO) {
|
||||
val repository = git.repository
|
||||
val dirCache = repository.lockDirCache()
|
||||
val dirCacheEditor = dirCache.editor()
|
||||
|
||||
val rawFileManager = rawFileManagerFactory.create(git.repository)
|
||||
val rawFile = rawFileManager.getRawContent(DiffEntry.Side.NEW, diffEntry)
|
||||
val textLines = getTextLines(rawFile).toMutableList()
|
||||
|
||||
val hunkLines = hunk.lines.filter { it.lineType != LineType.CONTEXT }
|
||||
|
||||
val addedLines = hunkLines
|
||||
.filter { it.lineType == LineType.ADDED }
|
||||
.sortedBy { it.newLineNumber }
|
||||
val removedLines = hunkLines
|
||||
.filter { it.lineType == LineType.REMOVED }
|
||||
.sortedBy { it.newLineNumber }
|
||||
|
||||
var linesRemoved = 0
|
||||
|
||||
// Start by removing the added lines to the index
|
||||
for (line in addedLines) {
|
||||
textLines.removeAt(line.newLineNumber + linesRemoved)
|
||||
linesRemoved--
|
||||
}
|
||||
|
||||
var linesAdded = 0
|
||||
|
||||
// Restore previously removed lines to the index
|
||||
for (line in removedLines) {
|
||||
textLines.add(line.newLineNumber + linesAdded, line.text.withoutLineEnding)
|
||||
linesAdded++
|
||||
}
|
||||
|
||||
val stagedFileText = textLines.joinToString(rawFile.lineDelimiter)
|
||||
dirCacheEditor.add(HunkEdit(diffEntry.newPath, repository, ByteBuffer.wrap(stagedFileText.toByteArray())))
|
||||
dirCacheEditor.commit()
|
||||
|
||||
loadStatus(git)
|
||||
}
|
||||
|
||||
private fun getTextLines(rawFile: RawText): List<String> {
|
||||
val content = rawFile.rawContent.toString(Charsets.UTF_8)
|
||||
return content.split(rawFile.lineDelimiter).toMutableList()
|
||||
}
|
||||
|
||||
private class HunkEdit(
|
||||
path: String?,
|
||||
private val repo: Repository,
|
||||
private val content: ByteBuffer,
|
||||
) : PathEdit(path) {
|
||||
override fun apply(ent: DirCacheEntry) {
|
||||
val inserter: ObjectInserter = repo.newObjectInserter()
|
||||
if (ent.rawMode and FileMode.TYPE_MASK != FileMode.TYPE_FILE) {
|
||||
ent.fileMode = FileMode.REGULAR_FILE
|
||||
}
|
||||
ent.length = content.limit()
|
||||
ent.setLastModified(Instant.now())
|
||||
try {
|
||||
val `in` = ByteArrayInputStream(
|
||||
content.array(), 0, content.limit()
|
||||
)
|
||||
ent.setObjectId(
|
||||
inserter.insert(
|
||||
Constants.OBJ_BLOB, content.limit().toLong(),
|
||||
`in`
|
||||
)
|
||||
)
|
||||
inserter.flush()
|
||||
} catch (ex: IOException) {
|
||||
throw RuntimeException(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unstage(git: Git, diffEntry: DiffEntry) = withContext(Dispatchers.IO) {
|
||||
git.reset()
|
||||
|
@ -2,8 +2,10 @@ package app.git.diff
|
||||
|
||||
data class Hunk(val header: String, val lines: List<Line>)
|
||||
|
||||
sealed class Line(val content: String) {
|
||||
class ContextLine(content: String, val oldLineNumber: Int, val newLineNumber: Int): Line(content)
|
||||
class AddedLine(content: String, val oldLineNumber: Int, val newLineNumber: Int): Line(content)
|
||||
class RemovedLine(content: String, val lineNumber: Int): Line(content)
|
||||
data class Line(val text: String, val oldLineNumber: Int, val newLineNumber: Int, val lineType: LineType)
|
||||
|
||||
enum class LineType {
|
||||
CONTEXT,
|
||||
ADDED,
|
||||
REMOVED,
|
||||
}
|
@ -72,18 +72,18 @@ class HunkDiffGenerator @AssistedInject constructor(
|
||||
while (oldCurrentLine < oldEndLine || newCurrentLine < newEndLine) {
|
||||
if (oldCurrentLine < curEdit.beginA || endIdx + 1 < curIdx) {
|
||||
val lineText = oldRawText.lineAt(oldCurrentLine)
|
||||
lines.add(Line.ContextLine(lineText, oldCurrentLine, newCurrentLine))
|
||||
lines.add(Line(lineText, oldCurrentLine, newCurrentLine, LineType.CONTEXT))
|
||||
|
||||
oldCurrentLine++
|
||||
newCurrentLine++
|
||||
} else if (oldCurrentLine < curEdit.endA) {
|
||||
val lineText = oldRawText.lineAt(oldCurrentLine)
|
||||
lines.add(Line.RemovedLine(lineText, oldCurrentLine))
|
||||
lines.add(Line(lineText, oldCurrentLine, newCurrentLine, LineType.REMOVED))
|
||||
|
||||
oldCurrentLine++
|
||||
} else if (newCurrentLine < curEdit.endB) {
|
||||
val lineText = newRawText.lineAt(newCurrentLine)
|
||||
lines.add(Line.AddedLine(lineText, oldCurrentLine, newCurrentLine))
|
||||
lines.add(Line(lineText, oldCurrentLine, newCurrentLine, LineType.ADDED))
|
||||
|
||||
newCurrentLine++
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
package app.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
@ -21,9 +18,11 @@ import androidx.compose.ui.unit.sp
|
||||
import app.git.DiffEntryType
|
||||
import app.git.GitManager
|
||||
import app.git.diff.Hunk
|
||||
import app.git.diff.Line
|
||||
import app.git.diff.LineType
|
||||
import app.theme.primaryTextColor
|
||||
import app.ui.components.ScrollableLazyColumn
|
||||
import app.ui.components.SecondaryButton
|
||||
import org.eclipse.jgit.diff.DiffEntry
|
||||
|
||||
@Composable
|
||||
fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView: () -> Unit) {
|
||||
@ -54,39 +53,78 @@ fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView:
|
||||
) {
|
||||
Text("Close diff")
|
||||
}
|
||||
SelectionContainer {
|
||||
ScrollableLazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
items(text) { hunk ->
|
||||
|
||||
ScrollableLazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
// .padding(16.dp)
|
||||
) {
|
||||
itemsIndexed(text) { index, hunk ->
|
||||
val hunksSeparation = if (index == 0)
|
||||
0.dp
|
||||
else
|
||||
16.dp
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = hunksSeparation)
|
||||
.background(MaterialTheme.colors.surface)
|
||||
.padding(vertical = 4.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = hunk.header,
|
||||
color = MaterialTheme.colors.primaryTextColor,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.surface)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (
|
||||
(diffEntryType is DiffEntryType.StagedDiff || diffEntryType is DiffEntryType.UnstagedDiff) &&
|
||||
diffEntryType.diffEntry.changeType == DiffEntry.ChangeType.MODIFY
|
||||
) {
|
||||
val buttonText: String
|
||||
val color: Color
|
||||
if (diffEntryType is DiffEntryType.StagedDiff) {
|
||||
buttonText = "Unstage"
|
||||
color = MaterialTheme.colors.error
|
||||
} else {
|
||||
buttonText = "Stage"
|
||||
color = MaterialTheme.colors.primary
|
||||
}
|
||||
|
||||
SecondaryButton(
|
||||
text = buttonText,
|
||||
backgroundButton = color,
|
||||
onClick = {
|
||||
if (diffEntryType is DiffEntryType.StagedDiff) {
|
||||
gitManager.unstageHunk(diffEntryType.diffEntry, hunk)
|
||||
} else {
|
||||
gitManager.stageHunk(diffEntryType.diffEntry, hunk)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SelectionContainer {
|
||||
Column {
|
||||
hunk.lines.forEach { line ->
|
||||
val backgroundColor = when (line) {
|
||||
is Line.AddedLine -> {
|
||||
val backgroundColor = when (line.lineType) {
|
||||
LineType.ADDED -> {
|
||||
Color(0x77a9d49b)
|
||||
}
|
||||
is Line.RemovedLine -> {
|
||||
LineType.REMOVED -> {
|
||||
Color(0x77dea2a2)
|
||||
}
|
||||
is Line.ContextLine -> {
|
||||
LineType.CONTEXT -> {
|
||||
MaterialTheme.colors.background
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = line.content,
|
||||
text = line.text,
|
||||
modifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.padding(start = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
color = MaterialTheme.colors.primaryTextColor,
|
||||
maxLines = 1,
|
||||
|
@ -9,13 +9,11 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.input.key.Key
|
||||
@ -38,6 +36,7 @@ import app.theme.headerBackground
|
||||
import app.theme.headerText
|
||||
import app.theme.primaryTextColor
|
||||
import app.ui.components.ScrollableLazyColumn
|
||||
import app.ui.components.SecondaryButton
|
||||
import org.eclipse.jgit.diff.DiffEntry
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
|
||||
@ -50,6 +49,7 @@ fun UncommitedChanges(
|
||||
val stageStatusState = gitManager.stageStatus.collectAsState()
|
||||
val stageStatus = stageStatusState.value
|
||||
val lastCheck by gitManager.lastTimeChecked.collectAsState()
|
||||
val repositoryState by gitManager.repositoryState.collectAsState()
|
||||
|
||||
LaunchedEffect(lastCheck) {
|
||||
gitManager.loadStatus()
|
||||
@ -201,22 +201,12 @@ private fun EntriesList(
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.clip(RoundedCornerShape(5.dp))
|
||||
.background(actionColor)
|
||||
.clickable { onAllAction() },
|
||||
) {
|
||||
Text(
|
||||
text = allActionTitle,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colors.contentColorFor(actionColor),
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
SecondaryButton(
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
text = allActionTitle,
|
||||
backgroundButton = actionColor,
|
||||
onClick = onAllAction
|
||||
)
|
||||
}
|
||||
|
||||
ScrollableLazyColumn(
|
||||
|
39
src/main/kotlin/app/ui/components/SecondaryButton.kt
Normal file
39
src/main/kotlin/app/ui/components/SecondaryButton.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package app.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun SecondaryButton(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
backgroundButton: Color,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(5.dp))
|
||||
.background(backgroundButton)
|
||||
.clickable { onClick() },
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colors.contentColorFor(backgroundButton),
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user