Added basic images diff

This commit is contained in:
Abdelilah El Aissaoui 2022-01-29 20:33:08 +01:00
parent 57e428a9e8
commit 27f216aa5d
6 changed files with 171 additions and 53 deletions

View File

@ -3,8 +3,8 @@ package app.git
import app.di.HunkDiffGeneratorFactory import app.di.HunkDiffGeneratorFactory
import app.di.RawFileManagerFactory import app.di.RawFileManagerFactory
import app.extensions.fullData import app.extensions.fullData
import app.git.diff.DiffResult
import app.git.diff.Hunk 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.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@ -26,13 +26,12 @@ class DiffManager @Inject constructor(
private val rawFileManagerFactory: RawFileManagerFactory, private val rawFileManagerFactory: RawFileManagerFactory,
private val hunkDiffGeneratorFactory: HunkDiffGeneratorFactory, 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 diffEntry = diffEntryType.diffEntry
val byteArrayOutputStream = ByteArrayOutputStream() val byteArrayOutputStream = ByteArrayOutputStream()
val repository = git.repository val repository = git.repository
DiffFormatter(byteArrayOutputStream).use { formatter -> DiffFormatter(byteArrayOutputStream).use { formatter ->
formatter.setRepository(repository) formatter.setRepository(repository)
val oldTree = DirCacheIterator(repository.readDirCache()) val oldTree = DirCacheIterator(repository.readDirCache())
@ -49,7 +48,7 @@ class DiffManager @Inject constructor(
val rawFileManager = rawFileManagerFactory.create(repository) val rawFileManager = rawFileManagerFactory.create(repository)
val hunkDiffGenerator = hunkDiffGeneratorFactory.create(repository, rawFileManager) val hunkDiffGenerator = hunkDiffGeneratorFactory.create(repository, rawFileManager)
val hunks = mutableListOf<Hunk>() var diffResult: DiffResult
hunkDiffGenerator.use { hunkDiffGenerator.use {
if (diffEntryType is DiffEntryType.UnstagedDiff) { if (diffEntryType is DiffEntryType.UnstagedDiff) {
@ -58,10 +57,10 @@ class DiffManager @Inject constructor(
hunkDiffGenerator.scan(oldTree, newTree) 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) { 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.ContentSource
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.diff.RawText import org.eclipse.jgit.diff.RawText
import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.errors.BinaryBlobException
import org.eclipse.jgit.lib.FileMode import org.eclipse.jgit.lib.*
import org.eclipse.jgit.lib.ObjectReader
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.storage.pack.PackConfig import org.eclipse.jgit.storage.pack.PackConfig
import org.eclipse.jgit.treewalk.AbstractTreeIterator import org.eclipse.jgit.treewalk.AbstractTreeIterator
import org.eclipse.jgit.treewalk.WorkingTreeIterator import org.eclipse.jgit.treewalk.WorkingTreeIterator
import org.eclipse.jgit.util.LfsFactory 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 private const val DEFAULT_BINARY_FILE_THRESHOLD = PackConfig.DEFAULT_BIG_FILE_THRESHOLD
@ -22,6 +26,13 @@ class RawFileManager @AssistedInject constructor(
private var reader: ObjectReader = repository.newObjectReader() private var reader: ObjectReader = repository.newObjectReader()
private var source: ContentSource.Pair private var source: ContentSource.Pair
private val imageFormatsSupported = listOf(
"png",
"jpg",
"jpeg",
"svg"
)
init { init {
val cs = ContentSource.create(reader) val cs = ContentSource.create(reader)
source = ContentSource.Pair(cs, cs) source = ContentSource.Pair(cs, cs)
@ -38,18 +49,60 @@ class RawFileManager @AssistedInject constructor(
ContentSource.create(reader) ContentSource.create(reader)
} }
fun getRawContent(side: DiffEntry.Side, entry: DiffEntry): RawText { fun getRawContent(side: DiffEntry.Side, entry: DiffEntry): EntryContent {
if (entry.getMode(side) === FileMode.MISSING) return RawText.EMPTY_TEXT if (entry.getMode(side) === FileMode.MISSING) return EntryContent.Missing
if (entry.getMode(side).objectType != Constants.OBJ_BLOB) return RawText.EMPTY_TEXT if (entry.getMode(side).objectType != Constants.OBJ_BLOB) return EntryContent.InvalidObjectBlob
val ldr = LfsFactory.getInstance().applySmudgeFilter( val ldr = LfsFactory.getInstance().applySmudgeFilter(
repository, repository,
source.open(side, entry), entry.diffAttribute 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}_${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() { override fun close() {
reader.close() reader.close()
} }
}
sealed class EntryContent {
object Missing: EntryContent()
object InvalidObjectBlob: EntryContent()
data class Text(val rawText: RawText): EntryContent()
data class ImageBinary(val tempFilePath: Path): EntryContent()
object Binary: EntryContent()
object TooLargeEntry: EntryContent()
} }

View File

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

View File

@ -1,6 +1,7 @@
package app.git.diff package app.git.diff
import app.extensions.lineAt import app.extensions.lineAt
import app.git.EntryContent
import app.git.RawFileManager import app.git.RawFileManager
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -11,6 +12,7 @@ import org.eclipse.jgit.patch.FileHeader.PatchType
import org.eclipse.jgit.treewalk.AbstractTreeIterator import org.eclipse.jgit.treewalk.AbstractTreeIterator
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.nio.file.Path
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -37,11 +39,19 @@ class HunkDiffGenerator @AssistedInject constructor(
diffFormatter.scan(oldTreeIterator, newTreeIterator) diffFormatter.scan(oldTreeIterator, newTreeIterator)
} }
fun format(ent: DiffEntry): List<Hunk> { fun format(ent: DiffEntry): DiffResult {
val fileHeader = diffFormatter.toFileHeader(ent) val fileHeader = diffFormatter.toFileHeader(ent)
val rawOld = rawFileManager.getRawContent(DiffEntry.Side.OLD, ent) val rawOld = rawFileManager.getRawContent(DiffEntry.Side.OLD, ent)
val rawNew = rawFileManager.getRawContent(DiffEntry.Side.NEW, ent) val rawNew = rawFileManager.getRawContent(DiffEntry.Side.NEW, ent)
return format(fileHeader, rawOld, rawNew)
// todo won't work for new files
return if(rawOld is EntryContent.Text && rawNew is EntryContent.Text)
DiffResult.Text(format(fileHeader, rawOld.rawText, rawNew.rawText))
else if(rawOld is EntryContent.ImageBinary && rawNew is EntryContent.ImageBinary)
DiffResult.Images(rawOld.tempFilePath, rawNew.tempFilePath)
else
DiffResult.Text(emptyList())
} }
/** /**
@ -142,4 +152,9 @@ class HunkDiffGenerator @AssistedInject constructor(
private fun end(edit: Edit, a: Int, b: Int): Boolean { private fun end(edit: Edit, a: Int, b: Int): Boolean {
return edit.endA <= a && edit.endB <= b return edit.endA <= a && edit.endB <= b
} }
}
sealed class DiffResult {
data class Text(val hunks: List<Hunk>): DiffResult()
data class Images(val oldTempFile: Path, val newTempsFile: Path): DiffResult()
} }

View File

@ -12,11 +12,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.loadImageBitmap
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp 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.diff.DiffResult
import app.git.diff.Hunk import app.git.diff.Hunk
import app.git.diff.Line import app.git.diff.Line
import app.git.diff.LineType import app.git.diff.LineType
@ -25,6 +27,8 @@ import app.ui.components.ScrollableLazyColumn
import app.ui.components.SecondaryButton import app.ui.components.SecondaryButton
import app.viewmodels.DiffViewModel import app.viewmodels.DiffViewModel
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
import java.io.FileInputStream
import kotlin.io.path.absolutePathString
import kotlin.math.max import kotlin.math.max
@Composable @Composable
@ -33,11 +37,11 @@ fun Diff(
onCloseDiffView: () -> Unit, onCloseDiffView: () -> Unit,
) { ) {
val diffResultState = diffViewModel.diffResult.collectAsState() 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 diffEntry = diffEntryType.diffEntry
val hunks = diffResult.hunks val diffResult = viewDiffResult.diffResult
Column( Column(
modifier = Modifier modifier = Modifier
@ -46,35 +50,75 @@ fun Diff(
.fillMaxSize() .fillMaxSize()
) { ) {
DiffHeader(diffEntry, onCloseDiffView) DiffHeader(diffEntry, onCloseDiffView)
if (diffResult is DiffResult.Text) {
TextDiff(diffEntryType, diffViewModel, diffResult)
} else if (diffResult is DiffResult.Images) {
ImagesDiff(diffResult)
}
}
}
val scrollState by diffViewModel.lazyListState.collectAsState() @Composable
ScrollableLazyColumn( fun ImagesDiff(diffResult: DiffResult.Images) {
modifier = Modifier val oldImagePath = diffResult.oldTempFile
.fillMaxSize(), val newImagePath = diffResult.newTempsFile
state = scrollState Row(
) { modifier = Modifier
items(hunks) { hunk -> .fillMaxSize()
HunkHeader( .background(Color.Red),
hunk = hunk, verticalAlignment = Alignment.CenterVertically
diffEntryType = diffEntryType, ) {
diffViewModel = diffViewModel, Image(
) bitmap = loadImageBitmap(inputStream = FileInputStream(oldImagePath.absolutePathString())),
contentDescription = null,
modifier = Modifier.fillMaxWidth(0.5f)
.background(Color.Yellow),
)
Spacer(
modifier = Modifier.fillMaxWidth(0.1f)
.background(Color.Green),
)
Image(
bitmap = loadImageBitmap(inputStream = FileInputStream(newImagePath.absolutePathString())),
contentDescription = null,
modifier = Modifier.fillMaxWidth()
.background(Color.Blue),
)
}
}
SelectionContainer { @Composable
Column { fun TextDiff(diffEntryType: DiffEntryType, diffViewModel: DiffViewModel, diffResult: DiffResult.Text) {
val oldHighestLineNumber = hunk.lines.maxOf { it.displayOldLineNumber } val hunks = diffResult.hunks
val newHighestLineNumber = hunk.lines.maxOf { it.displayNewLineNumber }
val highestLineNumber = max(oldHighestLineNumber, newHighestLineNumber)
val highestLineNumberLength = highestLineNumber.toString().count()
hunk.lines.forEach { line -> val scrollState by diffViewModel.lazyListState.collectAsState()
DiffLine(highestLineNumberLength, line) 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 @Composable

View File

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