Added basic images diff
This commit is contained in:
parent
57e428a9e8
commit
27f216aa5d
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
@ -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()
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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<Hunk>): DiffResult()
|
||||
data class Images(val oldTempFile: Path, val newTempsFile: Path): DiffResult()
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
Loading…
Reference in New Issue
Block a user