Refactored diff by replacing text diff patch with a custom Hunk class

This commit is contained in:
Abdelilah El Aissaoui 2021-12-27 05:13:55 +01:00
parent 908696735b
commit bd719be963
6 changed files with 276 additions and 51 deletions

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

View File

@ -1,8 +1,11 @@
package app.git
import app.extensions.fullData
import app.git.diff.Hunk
import app.git.diff.HunkDiffGenerator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.commons.logging.LogFactory.objectId
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.diff.DiffFormatter
@ -17,18 +20,19 @@ import org.eclipse.jgit.treewalk.FileTreeIterator
import java.io.ByteArrayOutputStream
import javax.inject.Inject
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 byteArrayOutputStream = ByteArrayOutputStream()
val repository = git.repository
DiffFormatter(byteArrayOutputStream).use { formatter ->
val repo = git.repository
formatter.setRepository(repo)
formatter.setRepository(repository)
val oldTree = DirCacheIterator(repo.readDirCache())
val newTree = FileTreeIterator(repo)
val oldTree = DirCacheIterator(repository.readDirCache())
val newTree = FileTreeIterator(repository)
if (diffEntryType is DiffEntryType.UnstagedDiff)
formatter.scan(oldTree, newTree)
@ -38,22 +42,21 @@ class DiffManager @Inject constructor() {
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")
return@withContext diff.split("\n", "\r\n").filterNot {
it.startsWith("diff --git")
}.map {
if (containsWindowsNewLine)
"$it\r\n"
else
"$it\n"
hunks.addAll(hunkDiffGenerator.format(diffEntry))
}
}
return@withContext hunks
}
suspend fun commitDiffEntries(git: Git, commit: RevCommit): List<DiffEntry> = withContext(Dispatchers.IO) {
val fullCommit = commit.fullData(git.repository) ?: return@withContext emptyList()

View File

@ -5,6 +5,7 @@ import app.app.ErrorsManager
import app.app.newErrorNow
import app.credentials.CredentialsState
import app.credentials.CredentialsStateManager
import app.git.diff.Hunk
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -188,7 +189,7 @@ class GitManager @Inject constructor(
val hasUncommitedChanges: StateFlow<Boolean>
get() = statusManager.hasUncommitedChanges
suspend fun diffFormat(diffEntryType: DiffEntryType): List<String> {
suspend fun diffFormat(diffEntryType: DiffEntryType): List<Hunk> {
try {
return diffManager.diffFormat(safeGit, diffEntryType)
} catch (ex: Exception) {

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

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

View File

@ -20,12 +20,14 @@ import androidx.compose.ui.unit.dp
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.theme.primaryTextColor
import app.ui.components.ScrollableLazyColumn
@Composable
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) {
text = gitManager.diffFormat(diffEntryType)
@ -58,44 +60,44 @@ fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView:
.fillMaxSize()
.padding(16.dp)
) {
items(text) { line ->
val isHunkLine = line.startsWith("@@")
items(text) { hunk ->
Text(
text = hunk.header,
color = MaterialTheme.colors.primaryTextColor,
modifier = Modifier
.background(MaterialTheme.colors.surface)
.fillMaxWidth(),
)
val backgroundColor = when {
line.startsWith("+") -> {
Color(0x77a9d49b)
}
line.startsWith("-") -> {
Color(0x77dea2a2)
}
isHunkLine -> {
MaterialTheme.colors.surface
}
else -> {
MaterialTheme.colors.background
Column {
hunk.lines.forEach { line ->
val backgroundColor = when (line) {
is Line.AddedLine -> {
Color(0x77a9d49b)
}
is Line.RemovedLine -> {
Color(0x77dea2a2)
}
is Line.ContextLine -> {
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,
)
}
}
}
}
}