Merge branch 'images_diff'

This commit is contained in:
Abdelilah El Aissaoui 2022-01-30 02:29:15 +01:00
commit ca1eeb2d11
7 changed files with 278 additions and 53 deletions

View File

@ -3,8 +3,8 @@ package app.git
import app.di.HunkDiffGeneratorFactory
import app.di.RawFileManagerFactory
import app.extensions.fullData
import app.git.diff.DiffResult
import app.git.diff.Hunk
import app.git.diff.HunkDiffGenerator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
@ -26,13 +26,12 @@ class DiffManager @Inject constructor(
private val rawFileManagerFactory: RawFileManagerFactory,
private val hunkDiffGeneratorFactory: HunkDiffGeneratorFactory,
) {
suspend fun diffFormat(git: Git, diffEntryType: DiffEntryType): List<Hunk> = withContext(Dispatchers.IO) {
suspend fun diffFormat(git: Git, diffEntryType: DiffEntryType): DiffResult = withContext(Dispatchers.IO) {
val diffEntry = diffEntryType.diffEntry
val byteArrayOutputStream = ByteArrayOutputStream()
val repository = git.repository
DiffFormatter(byteArrayOutputStream).use { formatter ->
formatter.setRepository(repository)
val oldTree = DirCacheIterator(repository.readDirCache())
@ -49,7 +48,7 @@ class DiffManager @Inject constructor(
val rawFileManager = rawFileManagerFactory.create(repository)
val hunkDiffGenerator = hunkDiffGeneratorFactory.create(repository, rawFileManager)
val hunks = mutableListOf<Hunk>()
var diffResult: DiffResult
hunkDiffGenerator.use {
if (diffEntryType is DiffEntryType.UnstagedDiff) {
@ -58,10 +57,10 @@ class DiffManager @Inject constructor(
hunkDiffGenerator.scan(oldTree, newTree)
}
hunks.addAll(hunkDiffGenerator.format(diffEntry))
diffResult = hunkDiffGenerator.format(diffEntry)
}
return@withContext hunks
return@withContext diffResult
}
suspend fun commitDiffEntries(git: Git, commit: RevCommit): List<DiffEntry> = withContext(Dispatchers.IO) {

View File

@ -5,14 +5,18 @@ import dagger.assisted.AssistedInject
import org.eclipse.jgit.diff.ContentSource
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.diff.RawText
import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.lib.ObjectReader
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.errors.BinaryBlobException
import org.eclipse.jgit.lib.*
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.FileOutputStream
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.createTempDirectory
import kotlin.io.path.createTempFile
private const val DEFAULT_BINARY_FILE_THRESHOLD = PackConfig.DEFAULT_BIG_FILE_THRESHOLD
@ -22,6 +26,12 @@ class RawFileManager @AssistedInject constructor(
private var reader: ObjectReader = repository.newObjectReader()
private var source: ContentSource.Pair
private val imageFormatsSupported = listOf(
"png",
"jpg",
"jpeg",
)
init {
val cs = ContentSource.create(reader)
source = ContentSource.Pair(cs, cs)
@ -38,18 +48,61 @@ class RawFileManager @AssistedInject constructor(
ContentSource.create(reader)
}
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
fun getRawContent(side: DiffEntry.Side, entry: DiffEntry): EntryContent {
if (entry.getMode(side) === FileMode.MISSING) return EntryContent.Missing
if (entry.getMode(side).objectType != Constants.OBJ_BLOB) return EntryContent.InvalidObjectBlob
val ldr = LfsFactory.getInstance().applySmudgeFilter(
repository,
source.open(side, entry), entry.diffAttribute
)
return RawText.load(ldr, DEFAULT_BINARY_FILE_THRESHOLD)
return try {
EntryContent.Text(RawText.load(ldr, DEFAULT_BINARY_FILE_THRESHOLD))
} catch (ex: BinaryBlobException) {
if(isImage(entry)) {
generateImageBinary(ldr, entry, side)
} else
EntryContent.Binary
}
}
private fun generateImageBinary(ldr: ObjectLoader, entry: DiffEntry, side: DiffEntry.Side): EntryContent.ImageBinary {
println("Data's size is ${ldr.size}")
val tempDir = createTempDirectory("gitnuro${repository.directory.absolutePath.replace("/", "_")}")
val tempFile = createTempFile(tempDir, prefix = "${entry.newPath.replace("/", "_")}_${side.name}")
println("Temp file generated: ${tempFile.absolutePathString()}")
val out = FileOutputStream(tempFile.toFile())
out.use {
ldr.copyTo(out)
}
return EntryContent.ImageBinary(tempFile)
}
// todo check if it's an image checking the binary format, checking the extension is a temporary workaround
private fun isImage(entry: DiffEntry): Boolean {
val path = entry.newPath
val fileExtension = path.split(".").lastOrNull() ?: return false
return imageFormatsSupported.contains(fileExtension)
}
// fun isBinary() = RawText.isBinary()
override fun close() {
reader.close()
}
}
sealed class EntryContent {
object Missing: EntryContent()
object InvalidObjectBlob: EntryContent()
data class Text(val rawText: RawText): EntryContent()
sealed class BinaryContent() : EntryContent()
data class ImageBinary(val tempFilePath: Path): BinaryContent()
object Binary: BinaryContent()
object TooLargeEntry: EntryContent()
}

View File

@ -12,8 +12,6 @@ import app.git.diff.Hunk
import app.git.diff.LineType
import app.theme.conflictFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.diff.DiffEntry
@ -60,8 +58,12 @@ class StatusManager @Inject constructor(
try {
val rawFileManager = rawFileManagerFactory.create(git.repository)
val rawFile = rawFileManager.getRawContent(DiffEntry.Side.OLD, diffEntry)
val textLines = getTextLines(rawFile).toMutableList()
val entryContent = rawFileManager.getRawContent(DiffEntry.Side.OLD, diffEntry)
if(entryContent !is EntryContent.Text)
return@withContext
val textLines = getTextLines(entryContent.rawText).toMutableList()
val hunkLines = hunk.lines.filter { it.lineType != LineType.CONTEXT }
@ -80,7 +82,7 @@ class StatusManager @Inject constructor(
}
}
val stagedFileText = textLines.joinToString(rawFile.lineDelimiter)
val stagedFileText = textLines.joinToString(entryContent.rawText.lineDelimiter)
dirCacheEditor.add(HunkEdit(diffEntry.newPath, repository, ByteBuffer.wrap(stagedFileText.toByteArray())))
dirCacheEditor.commit()
@ -99,8 +101,12 @@ class StatusManager @Inject constructor(
try {
val rawFileManager = rawFileManagerFactory.create(git.repository)
val rawFile = rawFileManager.getRawContent(DiffEntry.Side.NEW, diffEntry)
val textLines = getTextLines(rawFile).toMutableList()
val entryContent = rawFileManager.getRawContent(DiffEntry.Side.NEW, diffEntry)
if(entryContent !is EntryContent.Text)
return@withContext
val textLines = getTextLines(entryContent.rawText).toMutableList()
val hunkLines = hunk.lines.filter { it.lineType != LineType.CONTEXT }
@ -129,7 +135,7 @@ class StatusManager @Inject constructor(
linesAdded++
}
val stagedFileText = textLines.joinToString(rawFile.lineDelimiter)
val stagedFileText = textLines.joinToString(entryContent.rawText.lineDelimiter)
dirCacheEditor.add(HunkEdit(diffEntry.newPath, repository, ByteBuffer.wrap(stagedFileText.toByteArray())))
dirCacheEditor.commit()

View File

@ -1,6 +1,7 @@
package app.git.diff
import app.extensions.lineAt
import app.git.EntryContent
import app.git.RawFileManager
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -11,10 +12,14 @@ import org.eclipse.jgit.patch.FileHeader.PatchType
import org.eclipse.jgit.treewalk.AbstractTreeIterator
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InvalidObjectException
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.math.max
import kotlin.math.min
private const val CONTEXT_LINES = 3
/**
* Generator of [Hunk] lists from [DiffEntry]
*/
@ -37,11 +42,53 @@ class HunkDiffGenerator @AssistedInject constructor(
diffFormatter.scan(oldTreeIterator, newTreeIterator)
}
fun format(ent: DiffEntry): List<Hunk> {
fun format(ent: DiffEntry): DiffResult {
val fileHeader = diffFormatter.toFileHeader(ent)
val rawOld = rawFileManager.getRawContent(DiffEntry.Side.OLD, ent)
val rawNew = rawFileManager.getRawContent(DiffEntry.Side.NEW, ent)
return format(fileHeader, rawOld, rawNew)
if(rawOld == EntryContent.InvalidObjectBlob || rawNew == EntryContent.InvalidObjectBlob)
throw InvalidObjectException("Invalid object in diff format")
var diffResult: DiffResult = DiffResult.Text(emptyList())
// If we can, generate text diff (if one of the files has never been a binary file)
val hasGeneratedTextDiff = canGenerateTextDiff(rawOld, rawNew) { oldRawText, newRawText ->
diffResult = DiffResult.Text(format(fileHeader, oldRawText, newRawText))
}
if (!hasGeneratedTextDiff) {
diffResult = DiffResult.NonText(rawOld, rawNew)
}
return diffResult
}
@OptIn(ExperimentalContracts::class)
private fun canGenerateTextDiff(
rawOld: EntryContent,
rawNew: EntryContent,
onText: (oldRawText: RawText, newRawText: RawText) -> Unit
): Boolean {
val rawOldText = when (rawOld) {
is EntryContent.Text -> rawOld.rawText
EntryContent.Missing -> RawText.EMPTY_TEXT
else -> null
}
val newOldText = when (rawNew) {
is EntryContent.Text -> rawNew.rawText
EntryContent.Missing -> RawText.EMPTY_TEXT
else -> null
}
return if(rawOldText != null && newOldText != null) {
onText(rawOldText, newOldText)
true
} else
false
}
/**
@ -142,4 +189,12 @@ class HunkDiffGenerator @AssistedInject constructor(
private fun end(edit: Edit, a: Int, b: Int): Boolean {
return edit.endA <= a && edit.endB <= b
}
}
sealed class DiffResult {
data class Text(val hunks: List<Hunk>) : DiffResult()
data class NonText(
val oldBinaryContent: EntryContent,
val newBinaryContent: EntryContent,
) : DiffResult()
}

View File

@ -12,11 +12,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.loadImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.git.DiffEntryType
import app.git.EntryContent
import app.git.diff.DiffResult
import app.git.diff.Hunk
import app.git.diff.Line
import app.git.diff.LineType
@ -25,6 +28,9 @@ import app.ui.components.ScrollableLazyColumn
import app.ui.components.SecondaryButton
import app.viewmodels.DiffViewModel
import org.eclipse.jgit.diff.DiffEntry
import java.io.FileInputStream
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.math.max
@Composable
@ -33,11 +39,11 @@ fun Diff(
onCloseDiffView: () -> Unit,
) {
val diffResultState = diffViewModel.diffResult.collectAsState()
val diffResult = diffResultState.value ?: return
val viewDiffResult = diffResultState.value ?: return
val diffEntryType = diffResult.diffEntryType
val diffEntryType = viewDiffResult.diffEntryType
val diffEntry = diffEntryType.diffEntry
val hunks = diffResult.hunks
val diffResult = viewDiffResult.diffResult
Column(
modifier = Modifier
@ -46,35 +52,137 @@ fun Diff(
.fillMaxSize()
) {
DiffHeader(diffEntry, onCloseDiffView)
if (diffResult is DiffResult.Text) {
TextDiff(diffEntryType, diffViewModel, diffResult)
} else if (diffResult is DiffResult.NonText) {
NonTextDiff(diffResult)
}
}
}
val scrollState by diffViewModel.lazyListState.collectAsState()
ScrollableLazyColumn(
modifier = Modifier
.fillMaxSize(),
state = scrollState
) {
items(hunks) { hunk ->
HunkHeader(
hunk = hunk,
diffEntryType = diffEntryType,
diffViewModel = diffViewModel,
)
@Composable
fun NonTextDiff(diffResult: DiffResult.NonText) {
val oldBinaryContent = diffResult.oldBinaryContent
val newBinaryContent = diffResult.newBinaryContent
SelectionContainer {
Column {
val oldHighestLineNumber = hunk.lines.maxOf { it.displayOldLineNumber }
val newHighestLineNumber = hunk.lines.maxOf { it.displayNewLineNumber }
val highestLineNumber = max(oldHighestLineNumber, newHighestLineNumber)
val highestLineNumberLength = highestLineNumber.toString().count()
val showOldAndNew = oldBinaryContent != EntryContent.Missing && newBinaryContent != EntryContent.Missing
hunk.lines.forEach { line ->
DiffLine(highestLineNumberLength, line)
}
Row(
modifier = Modifier
.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
) {
if (showOldAndNew) {
Column(
modifier = Modifier.weight(0.5f)
.padding(start = 24.dp, end = 8.dp, top = 24.dp, bottom = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SideTitle("Old")
SideDiff(oldBinaryContent)
}
Column(
modifier = Modifier.weight(0.5f)
.padding(start = 8.dp, end = 24.dp, top = 24.dp, bottom = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SideTitle("New")
SideDiff(newBinaryContent)
}
} else if(oldBinaryContent != EntryContent.Missing) {
Box(
modifier = Modifier.fillMaxSize()
.padding(all = 24.dp),
) {
SideDiff(oldBinaryContent)
}
} else if(newBinaryContent != EntryContent.Missing) {
Column(
modifier = Modifier.fillMaxSize()
.padding(all = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
SideTitle("Binary file")
Spacer(modifier = Modifier.height(24.dp))
SideDiff(newBinaryContent)
}
}
}
}
@Composable
fun SideTitle(text: String) {
Text(
text = text,
fontSize = 20.sp,
color = MaterialTheme.colors.primaryTextColor,
)
}
@Composable
fun SideDiff(entryContent: EntryContent) {
when (entryContent) {
EntryContent.Binary -> BinaryDiff()
is EntryContent.ImageBinary -> ImageDiff(entryContent.tempFilePath)
else -> {}
// is EntryContent.Text -> //TODO maybe have a text view if the file was a binary before?
// TODO Show some info about this EntryContent.TooLargeEntry -> TODO()
}
}
@Composable
fun ImageDiff(tempImagePath: Path) {
Image(
bitmap = loadImageBitmap(inputStream = FileInputStream(tempImagePath.absolutePathString())),
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
@Composable
fun BinaryDiff() {
Image(
painter = painterResource("binary.svg"),
contentDescription = null,
modifier = Modifier.width(400.dp),
colorFilter = ColorFilter.tint(MaterialTheme.colors.primary)
)
}
@Composable
fun TextDiff(diffEntryType: DiffEntryType, diffViewModel: DiffViewModel, diffResult: DiffResult.Text) {
val hunks = diffResult.hunks
val scrollState by diffViewModel.lazyListState.collectAsState()
ScrollableLazyColumn(
modifier = Modifier
.fillMaxSize(),
state = scrollState
) {
items(hunks) { hunk ->
HunkHeader(
hunk = hunk,
diffEntryType = diffEntryType,
diffViewModel = diffViewModel,
)
SelectionContainer {
Column {
val oldHighestLineNumber = hunk.lines.maxOf { it.displayOldLineNumber }
val newHighestLineNumber = hunk.lines.maxOf { it.displayNewLineNumber }
val highestLineNumber = max(oldHighestLineNumber, newHighestLineNumber)
val highestLineNumberLength = highestLineNumber.toString().count()
hunk.lines.forEach { line ->
DiffLine(highestLineNumberLength, line)
}
}
}
}
}
}
@Composable

View File

@ -2,6 +2,7 @@ package app.viewmodels
import androidx.compose.foundation.lazy.LazyListState
import app.git.*
import app.git.diff.DiffResult
import app.git.diff.Hunk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -14,8 +15,8 @@ class DiffViewModel @Inject constructor(
private val statusManager: StatusManager,
) {
// TODO Maybe use a sealed class instead of a null to represent that a diff is not selected?
private val _diffResult = MutableStateFlow<DiffResult?>(null)
val diffResult: StateFlow<DiffResult?> = _diffResult
private val _diffResult = MutableStateFlow<ViewDiffResult?>(null)
val diffResult: StateFlow<ViewDiffResult?> = _diffResult
val lazyListState = MutableStateFlow(
LazyListState(
@ -44,10 +45,10 @@ class DiffViewModel @Inject constructor(
//TODO: Just a workaround when trying to diff binary files
try {
val hunks = diffManager.diffFormat(git, diffEntryType)
_diffResult.value = DiffResult(diffEntryType, hunks)
_diffResult.value = ViewDiffResult(diffEntryType, hunks)
} catch (ex: Exception) {
ex.printStackTrace()
_diffResult.value = DiffResult(diffEntryType, emptyList())
_diffResult.value = ViewDiffResult(diffEntryType, DiffResult.Text(emptyList()))
}
return@runOperation RefreshType.NONE
@ -66,4 +67,4 @@ class DiffViewModel @Inject constructor(
}
}
data class DiffResult(val diffEntryType: DiffEntryType, val hunks: List<Hunk>)
data class ViewDiffResult(val diffEntryType: DiffEntryType, val diffResult: DiffResult)

View File

@ -0,0 +1,3 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 0C4.47715 0 0 4.47715 0 10V110C0 115.523 4.47715 120 10 120H110C115.523 120 120 115.523 120 110V10C120 4.47715 115.523 0 110 0H10ZM59.666 55.0039V69.2773C59.666 73.1328 59.2148 76.5371 58.3125 79.4902C57.4375 82.416 56.166 84.8633 54.498 86.832C52.8301 88.8281 50.793 90.332 48.3867 91.3438C45.9805 92.3555 43.2734 92.8613 40.2656 92.8613C37.2852 92.8613 34.5918 92.3555 32.1855 91.3438C29.7793 90.332 27.7422 88.8281 26.0742 86.832C24.3789 84.8633 23.0664 82.416 22.1367 79.4902C21.2344 76.5371 20.7832 73.1328 20.7832 69.2773V55.0039C20.7832 51.1484 21.2344 47.7578 22.1367 44.832C23.0391 41.8789 24.3379 39.4043 26.0332 37.4082C27.7012 35.4395 29.7246 33.9492 32.1035 32.9375C34.5098 31.9258 37.2031 31.4199 40.1836 31.4199C43.1914 31.4199 45.8984 31.9258 48.3047 32.9375C50.7109 33.9492 52.7617 35.4395 54.457 37.4082C56.125 39.4043 57.4102 41.8789 58.3125 44.832C59.2148 47.7578 59.666 51.1484 59.666 55.0039ZM32.2676 62.4277V64.3555L48.1816 52.502C48.127 50.2598 47.8809 48.3457 47.4434 46.7598C47.0332 45.1465 46.418 43.8613 45.5977 42.9043C44.9688 42.1387 44.1895 41.5781 43.2598 41.2227C42.3574 40.8398 41.332 40.6484 40.1836 40.6484C38.8984 40.6484 37.7637 40.8945 36.7793 41.3867C35.8223 41.8516 35.0156 42.5625 34.3594 43.5195C33.6484 44.5312 33.1152 45.8438 32.7598 47.457C32.4316 49.0703 32.2676 50.9844 32.2676 53.1992V59.8027V62.4277ZM47.6484 76.7832C48.0039 75.1426 48.1816 73.2148 48.1816 71V64.0684V62.0996V60.2539L32.2676 72.0664C32.3223 73.8711 32.5 75.4707 32.8008 76.8652C33.1289 78.2598 33.5801 79.4219 34.1543 80.3516C34.8105 81.4727 35.6445 82.3066 36.6562 82.8535C37.668 83.4004 38.8711 83.6738 40.2656 83.6738C41.5508 83.6738 42.6855 83.4414 43.6699 82.9766C44.6543 82.4844 45.4746 81.7461 46.1309 80.7617C46.8145 79.7227 47.3203 78.3965 47.6484 76.7832ZM99.1641 32.2812V92H87.6387V46.1855L72.5449 51.5996V41.6738L98.5488 32.2812H99.1641Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB