376 lines
12 KiB
Kotlin
376 lines
12 KiB
Kotlin
package app.git
|
|
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.vector.ImageVector
|
|
import app.di.RawFileManagerFactory
|
|
import app.extensions.*
|
|
import app.git.diff.Hunk
|
|
import app.git.diff.LineType
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.withContext
|
|
import org.eclipse.jgit.api.Git
|
|
import org.eclipse.jgit.api.Status
|
|
import org.eclipse.jgit.diff.DiffEntry
|
|
import org.eclipse.jgit.diff.RawText
|
|
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit
|
|
import org.eclipse.jgit.dircache.DirCacheEntry
|
|
import org.eclipse.jgit.lib.Constants
|
|
import org.eclipse.jgit.lib.FileMode
|
|
import org.eclipse.jgit.lib.ObjectInserter
|
|
import org.eclipse.jgit.lib.Repository
|
|
import java.io.ByteArrayInputStream
|
|
import java.io.IOException
|
|
import java.nio.ByteBuffer
|
|
import java.time.Instant
|
|
import javax.inject.Inject
|
|
|
|
|
|
class StatusManager @Inject constructor(
|
|
private val rawFileManagerFactory: RawFileManagerFactory,
|
|
private val submodulesManager: SubmodulesManager,
|
|
) {
|
|
suspend fun hasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) {
|
|
val status = git
|
|
.status()
|
|
.call()
|
|
|
|
return@withContext status.hasUncommittedChanges() || status.hasUntrackedChanges()
|
|
}
|
|
|
|
suspend fun stage(git: Git, statusEntry: StatusEntry) = withContext(Dispatchers.IO) {
|
|
git.add()
|
|
.addFilepattern(statusEntry.filePath)
|
|
.setUpdate(statusEntry.statusType == StatusType.REMOVED)
|
|
.call()
|
|
}
|
|
|
|
suspend fun stageHunk(git: Git, diffEntry: DiffEntry, hunk: Hunk) = withContext(Dispatchers.IO) {
|
|
val repository = git.repository
|
|
val dirCache = repository.lockDirCache()
|
|
val dirCacheEditor = dirCache.editor()
|
|
var completedWithErrors = true
|
|
|
|
try {
|
|
val rawFileManager = rawFileManagerFactory.create(git.repository)
|
|
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 }
|
|
|
|
var linesAdded = 0
|
|
for (line in hunkLines) {
|
|
when (line.lineType) {
|
|
LineType.ADDED -> {
|
|
textLines.add(line.oldLineNumber + linesAdded, line.text)
|
|
linesAdded++
|
|
}
|
|
LineType.REMOVED -> {
|
|
textLines.removeAt(line.oldLineNumber + linesAdded)
|
|
linesAdded--
|
|
}
|
|
else -> throw NotImplementedError("Line type not implemented for stage hunk")
|
|
}
|
|
}
|
|
|
|
val stagedFileText = textLines.joinToString("")
|
|
dirCacheEditor.add(HunkEdit(diffEntry.newPath, repository, ByteBuffer.wrap(stagedFileText.toByteArray())))
|
|
dirCacheEditor.commit()
|
|
|
|
completedWithErrors = false
|
|
} finally {
|
|
if (completedWithErrors)
|
|
dirCache.unlock()
|
|
}
|
|
}
|
|
|
|
suspend fun unstageHunk(git: Git, diffEntry: DiffEntry, hunk: Hunk) = withContext(Dispatchers.IO) {
|
|
val repository = git.repository
|
|
val dirCache = repository.lockDirCache()
|
|
val dirCacheEditor = dirCache.editor()
|
|
var completedWithErrors = true
|
|
|
|
try {
|
|
val rawFileManager = rawFileManagerFactory.create(repository)
|
|
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 }
|
|
|
|
val addedLines = hunkLines
|
|
.filter { it.lineType == LineType.ADDED }
|
|
.sortedBy { it.newLineNumber }
|
|
val removedLines = hunkLines
|
|
.filter { it.lineType == LineType.REMOVED }
|
|
.sortedBy { it.newLineNumber }
|
|
|
|
var linesRemoved = 0
|
|
|
|
// Start by removing the added lines to the index
|
|
for (line in addedLines) {
|
|
textLines.removeAt(line.newLineNumber + linesRemoved)
|
|
linesRemoved--
|
|
}
|
|
|
|
var linesAdded = 0
|
|
|
|
// Restore previously removed lines to the index
|
|
for (line in removedLines) {
|
|
// Check how many lines before this one have been deleted
|
|
val previouslyRemovedLines = addedLines.count { it.newLineNumber < line.newLineNumber }
|
|
textLines.add(line.newLineNumber + linesAdded - previouslyRemovedLines, line.text)
|
|
linesAdded++
|
|
}
|
|
|
|
val stagedFileText = textLines.joinToString("")
|
|
dirCacheEditor.add(HunkEdit(diffEntry.newPath, repository, ByteBuffer.wrap(stagedFileText.toByteArray())))
|
|
dirCacheEditor.commit()
|
|
|
|
completedWithErrors = false
|
|
} finally {
|
|
if (completedWithErrors)
|
|
dirCache.unlock()
|
|
}
|
|
}
|
|
|
|
private fun getTextLines(rawFile: RawText): List<String> {
|
|
val content = rawFile.rawContent.toString(Charsets.UTF_8)//.removeSuffix(rawFile.lineDelimiter)
|
|
val lineDelimiter: String? = rawFile.lineDelimiter
|
|
|
|
var splitted: List<String> = if (lineDelimiter != null) {
|
|
content.split(lineDelimiter).toMutableList().apply {
|
|
if (this.last() == "")
|
|
removeLast()
|
|
}
|
|
} else {
|
|
listOf(content)
|
|
}
|
|
|
|
splitted = splitted.mapIndexed { index, line ->
|
|
val lineWithBreak = line + lineDelimiter.orEmpty()
|
|
|
|
if (index == splitted.count() - 1 && !content.endsWith(lineWithBreak)) {
|
|
line
|
|
} else
|
|
lineWithBreak
|
|
}
|
|
|
|
return splitted
|
|
}
|
|
|
|
private class HunkEdit(
|
|
path: String?,
|
|
private val repo: Repository,
|
|
private val content: ByteBuffer,
|
|
) : PathEdit(path) {
|
|
override fun apply(ent: DirCacheEntry) {
|
|
val inserter: ObjectInserter = repo.newObjectInserter()
|
|
if (ent.rawMode and FileMode.TYPE_MASK != FileMode.TYPE_FILE) {
|
|
ent.fileMode = FileMode.REGULAR_FILE
|
|
}
|
|
ent.length = content.limit()
|
|
ent.setLastModified(Instant.now())
|
|
try {
|
|
val `in` = ByteArrayInputStream(
|
|
content.array(), 0, content.limit()
|
|
)
|
|
ent.setObjectId(
|
|
inserter.insert(
|
|
Constants.OBJ_BLOB, content.limit().toLong(),
|
|
`in`
|
|
)
|
|
)
|
|
inserter.flush()
|
|
} catch (ex: IOException) {
|
|
throw RuntimeException(ex)
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun unstage(git: Git, statusEntry: StatusEntry) = withContext(Dispatchers.IO) {
|
|
git.reset()
|
|
.addPath(statusEntry.filePath)
|
|
.call()
|
|
}
|
|
|
|
suspend fun commit(git: Git, message: String, amend: Boolean) = withContext(Dispatchers.IO) {
|
|
git.commit()
|
|
.setMessage(message)
|
|
.setAllowEmpty(false)
|
|
.setAmend(amend)
|
|
.call()
|
|
}
|
|
|
|
suspend fun reset(git: Git, statusEntry: StatusEntry, staged: Boolean) = withContext(Dispatchers.IO) {
|
|
if (staged) {
|
|
git
|
|
.reset()
|
|
.addPath(statusEntry.filePath)
|
|
.call()
|
|
}
|
|
|
|
git
|
|
.checkout()
|
|
.addPath(statusEntry.filePath)
|
|
.call()
|
|
}
|
|
|
|
suspend fun unstageAll(git: Git) = withContext(Dispatchers.IO) {
|
|
git
|
|
.reset()
|
|
.call()
|
|
}
|
|
|
|
suspend fun stageAll(git: Git) = withContext(Dispatchers.IO) {
|
|
git
|
|
.add()
|
|
.addFilepattern(".")
|
|
.setUpdate(true) // Modified and deleted files
|
|
.call()
|
|
git
|
|
.add()
|
|
.addFilepattern(".")
|
|
.setUpdate(false) // For newly added files
|
|
.call()
|
|
}
|
|
|
|
suspend fun getStatus(git: Git) =
|
|
withContext(Dispatchers.IO) {
|
|
git
|
|
.status()
|
|
.call()
|
|
}
|
|
|
|
suspend fun getStaged(status: Status) =
|
|
withContext(Dispatchers.IO) {
|
|
val added = status.added.map {
|
|
StatusEntry(it, StatusType.ADDED)
|
|
}
|
|
val modified = status.changed.map {
|
|
StatusEntry(it, StatusType.MODIFIED)
|
|
}
|
|
val removed = status.removed.map {
|
|
StatusEntry(it, StatusType.REMOVED)
|
|
}
|
|
|
|
return@withContext flatListOf(
|
|
added,
|
|
modified,
|
|
removed,
|
|
)
|
|
}
|
|
|
|
suspend fun getUnstaged(status: Status) = withContext(Dispatchers.IO) {
|
|
// TODO Test uninitialized modules after the refactor
|
|
// val uninitializedSubmodules = submodulesManager.uninitializedSubmodules(git)
|
|
|
|
val added = status.untracked.map {
|
|
StatusEntry(it, StatusType.ADDED)
|
|
}
|
|
val modified = status.modified.map {
|
|
StatusEntry(it, StatusType.MODIFIED)
|
|
}
|
|
val removed = status.missing.map {
|
|
StatusEntry(it, StatusType.REMOVED)
|
|
}
|
|
val conflicting = status.conflicting.map {
|
|
StatusEntry(it, StatusType.CONFLICTING)
|
|
}
|
|
|
|
return@withContext flatListOf(
|
|
added,
|
|
modified,
|
|
removed,
|
|
conflicting,
|
|
)
|
|
}
|
|
|
|
suspend fun getStatusSummary(git: Git): StatusSummary {
|
|
val status = getStatus(git)
|
|
val staged = getStaged(status)
|
|
val allChanges = staged.toMutableList()
|
|
|
|
val unstaged = getUnstaged(status)
|
|
|
|
allChanges.addAll(unstaged)
|
|
val groupedChanges = allChanges.groupBy {
|
|
|
|
}
|
|
val changesGrouped = groupedChanges.map {
|
|
it.value
|
|
}.flatten()
|
|
.groupBy {
|
|
it.statusType
|
|
}
|
|
|
|
val deletedCount = changesGrouped[StatusType.REMOVED].countOrZero()
|
|
val addedCount = changesGrouped[StatusType.ADDED].countOrZero()
|
|
|
|
val modifiedCount = changesGrouped[StatusType.MODIFIED].countOrZero()
|
|
val conflictingCount = changesGrouped[StatusType.CONFLICTING].countOrZero()
|
|
|
|
return StatusSummary(
|
|
modifiedCount = modifiedCount,
|
|
deletedCount = deletedCount,
|
|
addedCount = addedCount,
|
|
conflictingCount = conflictingCount,
|
|
)
|
|
}
|
|
|
|
suspend fun stageUntrackedFiles(git: Git) = withContext(Dispatchers.IO) {
|
|
val diffEntries = git
|
|
.diff()
|
|
.setShowNameAndStatusOnly(true)
|
|
.call()
|
|
|
|
val addedEntries = diffEntries.filter { it.changeType == DiffEntry.ChangeType.ADD }
|
|
|
|
if (addedEntries.isNotEmpty()) {
|
|
val addCommand = git
|
|
.add()
|
|
|
|
for (entry in addedEntries) {
|
|
addCommand.addFilepattern(entry.newPath)
|
|
}
|
|
|
|
addCommand.call()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
data class StatusEntry(val filePath: String, val statusType: StatusType) {
|
|
val icon: ImageVector
|
|
get() = statusType.icon
|
|
|
|
val iconColor: Color
|
|
@Composable
|
|
get() = statusType.iconColor
|
|
}
|
|
|
|
enum class StatusType {
|
|
ADDED,
|
|
MODIFIED,
|
|
REMOVED,
|
|
CONFLICTING,
|
|
}
|
|
|
|
data class StatusSummary(
|
|
val modifiedCount: Int,
|
|
val deletedCount: Int,
|
|
val addedCount: Int,
|
|
val conflictingCount: Int,
|
|
) {
|
|
val total = modifiedCount +
|
|
deletedCount +
|
|
addedCount +
|
|
conflictingCount
|
|
} |