diff --git a/src/main/kotlin/app/git/DiffManager.kt b/src/main/kotlin/app/git/DiffManager.kt index 59e7b72..da858ed 100644 --- a/src/main/kotlin/app/git/DiffManager.kt +++ b/src/main/kotlin/app/git/DiffManager.kt @@ -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 = 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() + 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 = withContext(Dispatchers.IO) { diff --git a/src/main/kotlin/app/git/RawFileManager.kt b/src/main/kotlin/app/git/RawFileManager.kt index 459f279..45dfe05 100644 --- a/src/main/kotlin/app/git/RawFileManager.kt +++ b/src/main/kotlin/app/git/RawFileManager.kt @@ -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() } \ No newline at end of file diff --git a/src/main/kotlin/app/git/StatusManager.kt b/src/main/kotlin/app/git/StatusManager.kt index 9d4a9e4..76e6f35 100644 --- a/src/main/kotlin/app/git/StatusManager.kt +++ b/src/main/kotlin/app/git/StatusManager.kt @@ -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() diff --git a/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt b/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt index cb4658a..545b93c 100644 --- a/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt +++ b/src/main/kotlin/app/git/diff/HunkDiffGenerator.kt @@ -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 { + 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()) } /** @@ -142,4 +152,9 @@ 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): DiffResult() + data class Images(val oldTempFile: Path, val newTempsFile: Path): DiffResult() } \ No newline at end of file diff --git a/src/main/kotlin/app/ui/Diff.kt b/src/main/kotlin/app/ui/Diff.kt index 819e98a..6b6e22f 100644 --- a/src/main/kotlin/app/ui/Diff.kt +++ b/src/main/kotlin/app/ui/Diff.kt @@ -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 diff --git a/src/main/kotlin/app/viewmodels/DiffViewModel.kt b/src/main/kotlin/app/viewmodels/DiffViewModel.kt index 7ddff87..4399088 100644 --- a/src/main/kotlin/app/viewmodels/DiffViewModel.kt +++ b/src/main/kotlin/app/viewmodels/DiffViewModel.kt @@ -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(null) - val diffResult: StateFlow = _diffResult + private val _diffResult = MutableStateFlow(null) + val diffResult: StateFlow = _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) \ No newline at end of file +data class ViewDiffResult(val diffEntryType: DiffEntryType, val diffResult: DiffResult) \ No newline at end of file