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.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,13 @@ class RawFileManager @AssistedInject constructor(
private var reader: ObjectReader = repository.newObjectReader()
private var source: ContentSource.Pair
private val imageFormatsSupported = listOf(
"png",
"jpg",
"jpeg",
"svg"
)
init {
val cs = ContentSource.create(reader)
source = ContentSource.Pair(cs, cs)
@ -38,18 +49,60 @@ 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}_${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()
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.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,6 +12,7 @@ import org.eclipse.jgit.patch.FileHeader.PatchType
import org.eclipse.jgit.treewalk.AbstractTreeIterator
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.file.Path
import kotlin.math.max
import kotlin.math.min
@ -37,11 +39,19 @@ 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)
// 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())
}
/**
@ -143,3 +153,8 @@ class HunkDiffGenerator @AssistedInject constructor(
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.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.diff.DiffResult
import app.git.diff.Hunk
import app.git.diff.Line
import app.git.diff.LineType
@ -25,6 +27,8 @@ 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 kotlin.io.path.absolutePathString
import kotlin.math.max
@Composable
@ -33,11 +37,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 +50,75 @@ fun Diff(
.fillMaxSize()
) {
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()
ScrollableLazyColumn(
modifier = Modifier
.fillMaxSize(),
state = scrollState
) {
items(hunks) { hunk ->
HunkHeader(
hunk = hunk,
diffEntryType = diffEntryType,
diffViewModel = diffViewModel,
)
@Composable
fun ImagesDiff(diffResult: DiffResult.Images) {
val oldImagePath = diffResult.oldTempFile
val newImagePath = diffResult.newTempsFile
Row(
modifier = Modifier
.fillMaxSize()
.background(Color.Red),
verticalAlignment = Alignment.CenterVertically
) {
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 {
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()
@Composable
fun TextDiff(diffEntryType: DiffEntryType, diffViewModel: DiffViewModel, diffResult: DiffResult.Text) {
val hunks = diffResult.hunks
hunk.lines.forEach { line ->
DiffLine(highestLineNumberLength, line)
}
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)