Refactored diff by replacing text diff patch with a custom Hunk class
This commit is contained in:
parent
908696735b
commit
bd719be963
10
src/main/kotlin/app/extensions/RawTextExtensions.kt
Normal file
10
src/main/kotlin/app/extensions/RawTextExtensions.kt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package app.extensions
|
||||||
|
|
||||||
|
import org.eclipse.jgit.diff.RawText
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
fun RawText.lineAt(line: Int): String {
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
this.writeLine(outputStream, line)
|
||||||
|
return outputStream.toString(Charsets.UTF_8) + "\n"
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
package app.git
|
package app.git
|
||||||
|
|
||||||
import app.extensions.fullData
|
import app.extensions.fullData
|
||||||
|
import app.git.diff.Hunk
|
||||||
|
import app.git.diff.HunkDiffGenerator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.apache.commons.logging.LogFactory.objectId
|
||||||
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
|
||||||
@ -17,18 +20,19 @@ import org.eclipse.jgit.treewalk.FileTreeIterator
|
|||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
class DiffManager @Inject constructor() {
|
class DiffManager @Inject constructor() {
|
||||||
suspend fun diffFormat(git: Git, diffEntryType: DiffEntryType): List<String> = withContext(Dispatchers.IO) {
|
suspend fun diffFormat(git: Git, diffEntryType: DiffEntryType): List<Hunk> = withContext(Dispatchers.IO) {
|
||||||
val diffEntry = diffEntryType.diffEntry
|
val diffEntry = diffEntryType.diffEntry
|
||||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
|
val repository = git.repository
|
||||||
|
|
||||||
DiffFormatter(byteArrayOutputStream).use { formatter ->
|
DiffFormatter(byteArrayOutputStream).use { formatter ->
|
||||||
val repo = git.repository
|
|
||||||
|
|
||||||
formatter.setRepository(repo)
|
formatter.setRepository(repository)
|
||||||
|
|
||||||
val oldTree = DirCacheIterator(repo.readDirCache())
|
val oldTree = DirCacheIterator(repository.readDirCache())
|
||||||
val newTree = FileTreeIterator(repo)
|
val newTree = FileTreeIterator(repository)
|
||||||
|
|
||||||
if (diffEntryType is DiffEntryType.UnstagedDiff)
|
if (diffEntryType is DiffEntryType.UnstagedDiff)
|
||||||
formatter.scan(oldTree, newTree)
|
formatter.scan(oldTree, newTree)
|
||||||
@ -38,22 +42,21 @@ class DiffManager @Inject constructor() {
|
|||||||
formatter.flush()
|
formatter.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
val diff = byteArrayOutputStream.toString(Charsets.UTF_8)
|
val hunkDiffGenerator = HunkDiffGenerator(git.repository)
|
||||||
|
val hunks = mutableListOf<Hunk>()
|
||||||
|
|
||||||
// TODO This is just a workaround, try to find properly which lines have to be displayed by using a custom diff
|
hunkDiffGenerator.use {
|
||||||
|
if (diffEntryType is DiffEntryType.UnstagedDiff) {
|
||||||
|
val oldTree = DirCacheIterator(repository.readDirCache())
|
||||||
|
val newTree = FileTreeIterator(repository)
|
||||||
|
hunkDiffGenerator.scan(oldTree, newTree)
|
||||||
|
}
|
||||||
|
|
||||||
val containsWindowsNewLine = diff.contains("\r\n")
|
hunks.addAll(hunkDiffGenerator.format(diffEntry))
|
||||||
|
|
||||||
return@withContext diff.split("\n", "\r\n").filterNot {
|
|
||||||
it.startsWith("diff --git")
|
|
||||||
}.map {
|
|
||||||
if (containsWindowsNewLine)
|
|
||||||
"$it\r\n"
|
|
||||||
else
|
|
||||||
"$it\n"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
return@withContext hunks
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun commitDiffEntries(git: Git, commit: RevCommit): List<DiffEntry> = withContext(Dispatchers.IO) {
|
suspend fun commitDiffEntries(git: Git, commit: RevCommit): List<DiffEntry> = withContext(Dispatchers.IO) {
|
||||||
val fullCommit = commit.fullData(git.repository) ?: return@withContext emptyList()
|
val fullCommit = commit.fullData(git.repository) ?: return@withContext emptyList()
|
||||||
|
@ -5,6 +5,7 @@ import app.app.ErrorsManager
|
|||||||
import app.app.newErrorNow
|
import app.app.newErrorNow
|
||||||
import app.credentials.CredentialsState
|
import app.credentials.CredentialsState
|
||||||
import app.credentials.CredentialsStateManager
|
import app.credentials.CredentialsStateManager
|
||||||
|
import app.git.diff.Hunk
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@ -188,7 +189,7 @@ class GitManager @Inject constructor(
|
|||||||
val hasUncommitedChanges: StateFlow<Boolean>
|
val hasUncommitedChanges: StateFlow<Boolean>
|
||||||
get() = statusManager.hasUncommitedChanges
|
get() = statusManager.hasUncommitedChanges
|
||||||
|
|
||||||
suspend fun diffFormat(diffEntryType: DiffEntryType): List<String> {
|
suspend fun diffFormat(diffEntryType: DiffEntryType): List<Hunk> {
|
||||||
try {
|
try {
|
||||||
return diffManager.diffFormat(safeGit, diffEntryType)
|
return diffManager.diffFormat(safeGit, diffEntryType)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
|
9
src/main/kotlin/app/git/diff/Hunk.kt
Normal file
9
src/main/kotlin/app/git/diff/Hunk.kt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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 lineNumber: Int): Line(content)
|
||||||
|
class RemovedLine(content: String, val lineNumber: Int): Line(content)
|
||||||
|
}
|
200
src/main/kotlin/app/git/diff/HunkDiffGenerator.kt
Normal file
200
src/main/kotlin/app/git/diff/HunkDiffGenerator.kt
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
package app.git.diff
|
||||||
|
|
||||||
|
import app.extensions.lineAt
|
||||||
|
import org.eclipse.jgit.diff.*
|
||||||
|
import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm
|
||||||
|
import org.eclipse.jgit.lib.*
|
||||||
|
import org.eclipse.jgit.patch.FileHeader
|
||||||
|
import org.eclipse.jgit.patch.FileHeader.PatchType
|
||||||
|
import org.eclipse.jgit.storage.pack.PackConfig
|
||||||
|
import org.eclipse.jgit.treewalk.AbstractTreeIterator
|
||||||
|
import org.eclipse.jgit.treewalk.WorkingTreeIterator
|
||||||
|
import org.eclipse.jgit.util.LfsFactory
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
private const val DEFAULT_BINARY_FILE_THRESHOLD = PackConfig.DEFAULT_BIG_FILE_THRESHOLD
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generator of [Hunk] lists from [DiffEntry]
|
||||||
|
*/
|
||||||
|
class HunkDiffGenerator(
|
||||||
|
private val repository: Repository,
|
||||||
|
) : AutoCloseable {
|
||||||
|
private var reader: ObjectReader? = null
|
||||||
|
private lateinit var diffCfg: DiffConfig
|
||||||
|
private lateinit var diffAlgorithm: DiffAlgorithm
|
||||||
|
private var context = 3
|
||||||
|
private var binaryFileThreshold = DEFAULT_BINARY_FILE_THRESHOLD
|
||||||
|
private val outputStream = ByteArrayOutputStream() // Dummy output stream used for the diff formatter
|
||||||
|
private val diffFormatter = DiffFormatter(outputStream).apply {
|
||||||
|
setRepository(repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
setReader(repository.newObjectReader(), repository.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var source: ContentSource.Pair? = null
|
||||||
|
private var quotePaths: Boolean? = null
|
||||||
|
|
||||||
|
private fun setReader(reader: ObjectReader, cfg: Config) {
|
||||||
|
close()
|
||||||
|
this.reader = reader
|
||||||
|
diffCfg = cfg.get(DiffConfig.KEY)
|
||||||
|
if (quotePaths == null) {
|
||||||
|
quotePaths = java.lang.Boolean
|
||||||
|
.valueOf(
|
||||||
|
cfg.getBoolean(
|
||||||
|
ConfigConstants.CONFIG_CORE_SECTION,
|
||||||
|
ConfigConstants.CONFIG_KEY_QUOTE_PATH, true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val cs = ContentSource.create(reader)
|
||||||
|
source = ContentSource.Pair(cs, cs)
|
||||||
|
diffAlgorithm = DiffAlgorithm.getAlgorithm(
|
||||||
|
cfg.getEnum(
|
||||||
|
ConfigConstants.CONFIG_DIFF_SECTION, null,
|
||||||
|
ConfigConstants.CONFIG_KEY_ALGORITHM,
|
||||||
|
SupportedAlgorithm.HISTOGRAM
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
reader?.close()
|
||||||
|
outputStream.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scan(oldTreeIterator: AbstractTreeIterator, newTreeIterator: AbstractTreeIterator) {
|
||||||
|
source = ContentSource.Pair(source(oldTreeIterator), source(newTreeIterator))
|
||||||
|
diffFormatter.scan(oldTreeIterator, newTreeIterator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun source(iterator: AbstractTreeIterator): ContentSource {
|
||||||
|
return if (iterator is WorkingTreeIterator)
|
||||||
|
ContentSource.create(iterator)
|
||||||
|
else
|
||||||
|
ContentSource.create(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun format(ent: DiffEntry): List<Hunk> {
|
||||||
|
val fileHeader = diffFormatter.toFileHeader(ent)
|
||||||
|
val rawOld = getRawContent(DiffEntry.Side.OLD, ent)
|
||||||
|
val rawNew = getRawContent(DiffEntry.Side.NEW, ent)
|
||||||
|
return format(fileHeader, rawOld, rawNew)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRawContent(side: DiffEntry.Side, entry: DiffEntry): RawText {
|
||||||
|
if (entry.getMode(side) === FileMode.MISSING) return RawText.EMPTY_TEXT
|
||||||
|
if (entry.getMode(side).objectType != Constants.OBJ_BLOB) return RawText.EMPTY_TEXT
|
||||||
|
|
||||||
|
val ldr = LfsFactory.getInstance().applySmudgeFilter(
|
||||||
|
repository,
|
||||||
|
source!!.open(side, entry), entry.diffAttribute
|
||||||
|
)
|
||||||
|
return RawText.load(ldr, binaryFileThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a [FileHeader] and the both [RawText], generate a [List] of [Hunk]
|
||||||
|
*/
|
||||||
|
private fun format(head: FileHeader, oldRawText: RawText, newRawText: RawText): List<Hunk> {
|
||||||
|
return if (head.patchType == PatchType.UNIFIED)
|
||||||
|
format(head.toEditList(), oldRawText, newRawText)
|
||||||
|
else
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun format(edits: EditList, oldRawText: RawText, newRawText: RawText): List<Hunk> {
|
||||||
|
var curIdx = 0
|
||||||
|
val hunksList = mutableListOf<Hunk>()
|
||||||
|
while (curIdx < edits.size) {
|
||||||
|
var curEdit = edits[curIdx]
|
||||||
|
val endIdx = findCombinedEnd(edits, curIdx)
|
||||||
|
val endEdit = edits[endIdx]
|
||||||
|
var oldCurrentLine = max(0, curEdit.beginA - context)
|
||||||
|
var newCurrentLine = max(0, curEdit.beginB - context)
|
||||||
|
val oldEndLine = min(oldRawText.size(), endEdit.endA + context)
|
||||||
|
val newEndLine = min(newRawText.size(), endEdit.endB + context)
|
||||||
|
|
||||||
|
val headerText = createHunkHeader(oldCurrentLine, oldEndLine, newCurrentLine, newEndLine)
|
||||||
|
val lines = mutableListOf<Line>()
|
||||||
|
|
||||||
|
while (oldCurrentLine < oldEndLine || newCurrentLine < newEndLine) {
|
||||||
|
if (oldCurrentLine < curEdit.beginA || endIdx + 1 < curIdx) {
|
||||||
|
val lineText = oldRawText.lineAt(oldCurrentLine)
|
||||||
|
lines.add(Line.ContextLine(lineText, oldCurrentLine, newCurrentLine))
|
||||||
|
|
||||||
|
oldCurrentLine++
|
||||||
|
newCurrentLine++
|
||||||
|
} else if (oldCurrentLine < curEdit.endA) {
|
||||||
|
val lineText = oldRawText.lineAt(oldCurrentLine)
|
||||||
|
lines.add(Line.RemovedLine(lineText, oldCurrentLine))
|
||||||
|
|
||||||
|
oldCurrentLine++
|
||||||
|
} else if (newCurrentLine < curEdit.endB) {
|
||||||
|
val lineText = newRawText.lineAt(newCurrentLine)
|
||||||
|
lines.add(Line.AddedLine(lineText, newCurrentLine))
|
||||||
|
|
||||||
|
newCurrentLine++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end(curEdit, oldCurrentLine, newCurrentLine) && ++curIdx < edits.size) curEdit = edits[curIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
hunksList.add(Hunk(headerText, lines))
|
||||||
|
}
|
||||||
|
|
||||||
|
return hunksList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the hunk's header string like in git diff
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun createHunkHeader(
|
||||||
|
oldStartLine: Int,
|
||||||
|
oldEndLine: Int,
|
||||||
|
newStartLine: Int,
|
||||||
|
newEndLine: Int
|
||||||
|
): String {
|
||||||
|
val prefix = "@@"
|
||||||
|
val contentRemoved = createRange('-', oldStartLine + 1, oldEndLine - oldStartLine)
|
||||||
|
val contentAdded = createRange('+', newStartLine + 1, newEndLine - newStartLine)
|
||||||
|
val suffix = " @@"
|
||||||
|
|
||||||
|
return prefix + contentRemoved + contentAdded + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRange(symbol: Char, begin: Int, linesCount: Int): String {
|
||||||
|
return when (linesCount) {
|
||||||
|
0 -> " $symbol${begin - 1},0"
|
||||||
|
1 -> " $symbol$begin" // If the range is exactly one line, produce only the number.
|
||||||
|
else -> " $symbol$begin,$linesCount"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findCombinedEnd(edits: List<Edit>, i: Int): Int {
|
||||||
|
var end = i + 1
|
||||||
|
while (end < edits.size
|
||||||
|
&& (combineA(edits, end) || combineB(edits, end))
|
||||||
|
) end++
|
||||||
|
return end - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun combineA(e: List<Edit>, i: Int): Boolean {
|
||||||
|
return e[i].beginA - e[i - 1].endA <= 2 * context
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun combineB(e: List<Edit>, i: Int): Boolean {
|
||||||
|
return e[i].beginB - e[i - 1].endB <= 2 * context
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun end(edit: Edit, a: Int, b: Int): Boolean {
|
||||||
|
return edit.endA <= a && edit.endB <= b
|
||||||
|
}
|
||||||
|
}
|
@ -20,12 +20,14 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import app.git.DiffEntryType
|
import app.git.DiffEntryType
|
||||||
import app.git.GitManager
|
import app.git.GitManager
|
||||||
|
import app.git.diff.Hunk
|
||||||
|
import app.git.diff.Line
|
||||||
import app.theme.primaryTextColor
|
import app.theme.primaryTextColor
|
||||||
import app.ui.components.ScrollableLazyColumn
|
import app.ui.components.ScrollableLazyColumn
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView: () -> Unit) {
|
fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView: () -> Unit) {
|
||||||
var text by remember { mutableStateOf(listOf<String>()) }
|
var text by remember { mutableStateOf(listOf<Hunk>()) }
|
||||||
|
|
||||||
LaunchedEffect(diffEntryType.diffEntry) {
|
LaunchedEffect(diffEntryType.diffEntry) {
|
||||||
text = gitManager.diffFormat(diffEntryType)
|
text = gitManager.diffFormat(diffEntryType)
|
||||||
@ -58,44 +60,44 @@ fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView:
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
items(text) { line ->
|
items(text) { hunk ->
|
||||||
val isHunkLine = line.startsWith("@@")
|
Text(
|
||||||
|
text = hunk.header,
|
||||||
|
color = MaterialTheme.colors.primaryTextColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colors.surface)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
val backgroundColor = when {
|
Column {
|
||||||
line.startsWith("+") -> {
|
hunk.lines.forEach { line ->
|
||||||
Color(0x77a9d49b)
|
val backgroundColor = when (line) {
|
||||||
}
|
is Line.AddedLine -> {
|
||||||
line.startsWith("-") -> {
|
Color(0x77a9d49b)
|
||||||
Color(0x77dea2a2)
|
}
|
||||||
}
|
is Line.RemovedLine -> {
|
||||||
isHunkLine -> {
|
Color(0x77dea2a2)
|
||||||
MaterialTheme.colors.surface
|
}
|
||||||
}
|
is Line.ContextLine -> {
|
||||||
else -> {
|
MaterialTheme.colors.background
|
||||||
MaterialTheme.colors.background
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = line.content,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(backgroundColor)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colors.primaryTextColor,
|
||||||
|
maxLines = 1,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val paddingTop = if (isHunkLine)
|
|
||||||
32.dp
|
|
||||||
else
|
|
||||||
0.dp
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = line,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = paddingTop)
|
|
||||||
.background(backgroundColor)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
color = MaterialTheme.colors.primaryTextColor,
|
|
||||||
maxLines = 1,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user