Implemented stage/unstage by hunks

This commit is contained in:
Abdelilah El Aissaoui 2021-12-28 01:19:29 +01:00
parent 86fd61d0d7
commit a8ed01784d
8 changed files with 266 additions and 57 deletions

View File

@ -28,4 +28,9 @@ val String.dirPath: String
parts.joinToString("/")
} else
this
}
}
val String.withoutLineEnding: String
get() = this
.removeSuffix("\n")
.removeSuffix("\r\n")

View File

@ -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)

View File

@ -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()

View File

@ -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,
}

View File

@ -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++
}

View File

@ -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,

View File

@ -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(

View 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)
)
}
}