From 3b1486efb6479c84ca1de78f2160325a3bfe3533 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Fri, 26 Aug 2022 05:35:58 +0200 Subject: [PATCH] Refactored status manager into use cases --- .../app/extensions/DiffEntryExtensions.kt | 4 +- src/main/kotlin/app/git/DiffEntryType.kt | 2 + src/main/kotlin/app/git/StatusManager.kt | 438 ------------------ .../repository/GetRepositoryStateUseCase.kt | 1 - .../CheckHasUncommitedChangedUseCase.kt | 17 + .../app/git/workspace/DoCommitUseCase.kt | 17 + .../workspace/GetLinesFromRawTextUseCase.kt | 15 + .../git/workspace/GetLinesFromTextUseCase.kt | 27 ++ .../app/git/workspace/GetStagedUseCase.kt | 28 ++ .../git/workspace/GetStatusSummaryUseCase.kt | 36 ++ .../app/git/workspace/GetStatusUseCase.kt | 17 + .../app/git/workspace/GetUnstagedUseCase.kt | 34 ++ src/main/kotlin/app/git/workspace/HunkEdit.kt | 41 ++ .../app/git/workspace/ResetEntryUseCase.kt | 24 + .../app/git/workspace/ResetHunkUseCase.kt | 62 +++ .../app/git/workspace/StageAllUseCase.kt | 21 + .../app/git/workspace/StageEntryUseCase.kt | 15 + .../app/git/workspace/StageHunkUseCase.kt | 74 +++ .../workspace/StageUntrackedFileUseCase.kt | 29 ++ src/main/kotlin/app/git/workspace/Status.kt | 34 ++ .../app/git/workspace/UnstageAllUseCase.kt | 14 + .../app/git/workspace/UnstageEntryUseCase.kt | 15 + .../app/git/workspace/UnstageHunkUseCase.kt | 73 +++ src/main/kotlin/app/ui/UncommitedChanges.kt | 4 +- .../context_menu/StagedEntriesContextMenu.kt | 4 +- .../context_menu/StatusEntriesContextMenu.kt | 4 +- src/main/kotlin/app/ui/diff/Diff.kt | 2 + src/main/kotlin/app/ui/log/Log.kt | 4 +- .../kotlin/app/viewmodels/DiffViewModel.kt | 17 +- .../kotlin/app/viewmodels/LogViewModel.kt | 12 +- .../kotlin/app/viewmodels/MenuViewModel.kt | 7 +- .../kotlin/app/viewmodels/StatusViewModel.kt | 34 +- 32 files changed, 651 insertions(+), 475 deletions(-) delete mode 100644 src/main/kotlin/app/git/StatusManager.kt create mode 100644 src/main/kotlin/app/git/workspace/CheckHasUncommitedChangedUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/DoCommitUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/GetLinesFromRawTextUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/GetLinesFromTextUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/GetStagedUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/GetStatusSummaryUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/GetStatusUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/GetUnstagedUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/HunkEdit.kt create mode 100644 src/main/kotlin/app/git/workspace/ResetEntryUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/ResetHunkUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/StageAllUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/StageEntryUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/StageHunkUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/StageUntrackedFileUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/Status.kt create mode 100644 src/main/kotlin/app/git/workspace/UnstageAllUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/UnstageEntryUseCase.kt create mode 100644 src/main/kotlin/app/git/workspace/UnstageHunkUseCase.kt diff --git a/src/main/kotlin/app/extensions/DiffEntryExtensions.kt b/src/main/kotlin/app/extensions/DiffEntryExtensions.kt index 18acccf..b8c9e98 100644 --- a/src/main/kotlin/app/extensions/DiffEntryExtensions.kt +++ b/src/main/kotlin/app/extensions/DiffEntryExtensions.kt @@ -6,8 +6,8 @@ import androidx.compose.material.icons.filled.* import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import app.git.StatusEntry -import app.git.StatusType +import app.git.workspace.StatusEntry +import app.git.workspace.StatusType import app.theme.addFile import app.theme.conflictFile import app.theme.modifyFile diff --git a/src/main/kotlin/app/git/DiffEntryType.kt b/src/main/kotlin/app/git/DiffEntryType.kt index 125bae8..bea3c5c 100644 --- a/src/main/kotlin/app/git/DiffEntryType.kt +++ b/src/main/kotlin/app/git/DiffEntryType.kt @@ -2,6 +2,8 @@ package app.git import app.extensions.filePath import app.extensions.toStatusType +import app.git.workspace.StatusEntry +import app.git.workspace.StatusType import org.eclipse.jgit.diff.DiffEntry sealed class DiffEntryType { diff --git a/src/main/kotlin/app/git/StatusManager.kt b/src/main/kotlin/app/git/StatusManager.kt deleted file mode 100644 index 9357836..0000000 --- a/src/main/kotlin/app/git/StatusManager.kt +++ /dev/null @@ -1,438 +0,0 @@ -package app.git - -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -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.File -import java.io.FileWriter -import java.io.IOException -import java.nio.ByteBuffer -import java.time.Instant -import javax.inject.Inject - - -class StatusManager @Inject constructor( - private val rawFileManager: RawFileManager, -) { - 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 entryContent = rawFileManager.getRawContent( - repository = git.repository, - side = DiffEntry.Side.OLD, - entry = diffEntry, - oldTreeIterator = null, - newTreeIterator = null - ) - - 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 entryContent = rawFileManager.getRawContent( - repository = repository, - side = DiffEntry.Side.NEW, - entry = diffEntry, - oldTreeIterator = null, - newTreeIterator = null - ) - - 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 { - val content = rawFile.rawContent.toString(Charsets.UTF_8)//.removeSuffix(rawFile.lineDelimiter) - val lineDelimiter: String? = rawFile.lineDelimiter - - return getTextLines(content, lineDelimiter) - } - - private fun getTextLines(content: String, lineDelimiter: String?): List { - var splitted: List = 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 || statusEntry.statusType == StatusType.CONFLICTING) { - 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() - } - } - - suspend fun resetHunk(git: Git, diffEntry: DiffEntry, hunk: Hunk) = withContext(Dispatchers.IO) { - val repository = git.repository - - try { - val file = File(repository.directory.parent, diffEntry.oldPath) - - val content = file.readText() - val textLines = getTextLines(content, content.lineDelimiter).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("") - - - FileWriter(file).use { fw -> - fw.write(stagedFileText) - } - } catch (ex: Exception) { - throw Exception("Discard hunk failed. Check if the file still exists and has the write permissions set", ex) - } - } -} - - -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 -} \ No newline at end of file diff --git a/src/main/kotlin/app/git/repository/GetRepositoryStateUseCase.kt b/src/main/kotlin/app/git/repository/GetRepositoryStateUseCase.kt index 84affcf..2f57609 100644 --- a/src/main/kotlin/app/git/repository/GetRepositoryStateUseCase.kt +++ b/src/main/kotlin/app/git/repository/GetRepositoryStateUseCase.kt @@ -10,5 +10,4 @@ class GetRepositoryStateUseCase @Inject constructor() { suspend operator fun invoke(git: Git): RepositoryState = withContext(Dispatchers.IO) { return@withContext git.repository.repositoryState } - } \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/CheckHasUncommitedChangedUseCase.kt b/src/main/kotlin/app/git/workspace/CheckHasUncommitedChangedUseCase.kt new file mode 100644 index 0000000..4a82a65 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/CheckHasUncommitedChangedUseCase.kt @@ -0,0 +1,17 @@ +package app.git.workspace + +import app.extensions.hasUntrackedChanges +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import javax.inject.Inject + +class CheckHasUncommitedChangedUseCase @Inject constructor() { + suspend operator fun invoke(git: Git) = withContext(Dispatchers.IO) { + val status = git + .status() + .call() + + return@withContext status.hasUncommittedChanges() || status.hasUntrackedChanges() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/DoCommitUseCase.kt b/src/main/kotlin/app/git/workspace/DoCommitUseCase.kt new file mode 100644 index 0000000..921bf99 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/DoCommitUseCase.kt @@ -0,0 +1,17 @@ +package app.git.workspace + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.revwalk.RevCommit +import javax.inject.Inject + +class DoCommitUseCase @Inject constructor() { + suspend operator fun invoke(git: Git, message: String, amend: Boolean): RevCommit = withContext(Dispatchers.IO) { + git.commit() + .setMessage(message) + .setAllowEmpty(false) + .setAmend(amend) + .call() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/GetLinesFromRawTextUseCase.kt b/src/main/kotlin/app/git/workspace/GetLinesFromRawTextUseCase.kt new file mode 100644 index 0000000..d0011de --- /dev/null +++ b/src/main/kotlin/app/git/workspace/GetLinesFromRawTextUseCase.kt @@ -0,0 +1,15 @@ +package app.git.workspace + +import org.eclipse.jgit.diff.RawText +import javax.inject.Inject + +class GetLinesFromRawTextUseCase @Inject constructor( + private val getLinesFromTextUseCase: GetLinesFromTextUseCase, +) { + operator fun invoke(rawFile: RawText): List { + val content = rawFile.rawContent.toString(Charsets.UTF_8)//.removeSuffix(rawFile.lineDelimiter) + val lineDelimiter: String? = rawFile.lineDelimiter + + return getLinesFromTextUseCase(content, lineDelimiter) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/GetLinesFromTextUseCase.kt b/src/main/kotlin/app/git/workspace/GetLinesFromTextUseCase.kt new file mode 100644 index 0000000..28339cc --- /dev/null +++ b/src/main/kotlin/app/git/workspace/GetLinesFromTextUseCase.kt @@ -0,0 +1,27 @@ +package app.git.workspace + +import javax.inject.Inject + +class GetLinesFromTextUseCase @Inject constructor() { + operator fun invoke(content: String, lineDelimiter: String?): List { + var splitted: List = 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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/GetStagedUseCase.kt b/src/main/kotlin/app/git/workspace/GetStagedUseCase.kt new file mode 100644 index 0000000..4e150ef --- /dev/null +++ b/src/main/kotlin/app/git/workspace/GetStagedUseCase.kt @@ -0,0 +1,28 @@ +package app.git.workspace + +import app.extensions.flatListOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Status +import javax.inject.Inject + +class GetStagedUseCase @Inject constructor() { + suspend operator fun invoke(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, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/GetStatusSummaryUseCase.kt b/src/main/kotlin/app/git/workspace/GetStatusSummaryUseCase.kt new file mode 100644 index 0000000..6ff3208 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/GetStatusSummaryUseCase.kt @@ -0,0 +1,36 @@ +package app.git.workspace + +import app.extensions.countOrZero +import org.eclipse.jgit.api.Git +import javax.inject.Inject + +class GetStatusSummaryUseCase @Inject constructor( + private val getStagedUseCase: GetStagedUseCase, + private val getStatusUseCase: GetStatusUseCase, + private val getUnstagedUseCase: GetUnstagedUseCase, +) { + suspend operator fun invoke(git: Git): StatusSummary { + val status = getStatusUseCase(git) + val staged = getStagedUseCase(status) + + val unstaged = getUnstagedUseCase(status) + val allChanges = staged + unstaged + + val groupedChanges = allChanges.groupBy { + it.statusType + } + + val deletedCount = groupedChanges[StatusType.REMOVED].countOrZero() + val addedCount = groupedChanges[StatusType.ADDED].countOrZero() + + val modifiedCount = groupedChanges[StatusType.MODIFIED].countOrZero() + val conflictingCount = groupedChanges[StatusType.CONFLICTING].countOrZero() + + return StatusSummary( + modifiedCount = modifiedCount, + deletedCount = deletedCount, + addedCount = addedCount, + conflictingCount = conflictingCount, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/GetStatusUseCase.kt b/src/main/kotlin/app/git/workspace/GetStatusUseCase.kt new file mode 100644 index 0000000..3def83c --- /dev/null +++ b/src/main/kotlin/app/git/workspace/GetStatusUseCase.kt @@ -0,0 +1,17 @@ +package app.git.workspace + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.Status +import javax.inject.Inject + +class GetStatusUseCase @Inject constructor() { + suspend operator fun invoke(git: Git): Status = + withContext(Dispatchers.IO) { + git + .status() + .call() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/GetUnstagedUseCase.kt b/src/main/kotlin/app/git/workspace/GetUnstagedUseCase.kt new file mode 100644 index 0000000..46711e4 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/GetUnstagedUseCase.kt @@ -0,0 +1,34 @@ +package app.git.workspace + +import app.extensions.flatListOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Status +import javax.inject.Inject + +class GetUnstagedUseCase @Inject constructor() { + suspend operator fun invoke(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, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/HunkEdit.kt b/src/main/kotlin/app/git/workspace/HunkEdit.kt new file mode 100644 index 0000000..8bdf9e1 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/HunkEdit.kt @@ -0,0 +1,41 @@ +package app.git.workspace + +import org.eclipse.jgit.dircache.DirCacheEditor +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 + +class HunkEdit( + path: String?, + private val repo: Repository, + private val content: ByteBuffer, +) : DirCacheEditor.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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/ResetEntryUseCase.kt b/src/main/kotlin/app/git/workspace/ResetEntryUseCase.kt new file mode 100644 index 0000000..9b7bda8 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/ResetEntryUseCase.kt @@ -0,0 +1,24 @@ +package app.git.workspace + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Ref +import javax.inject.Inject + +class ResetEntryUseCase @Inject constructor() { + suspend operator fun invoke(git: Git, statusEntry: StatusEntry, staged: Boolean): Ref = + withContext(Dispatchers.IO) { + if (staged || statusEntry.statusType == StatusType.CONFLICTING) { + git + .reset() + .addPath(statusEntry.filePath) + .call() + } + + git + .checkout() + .addPath(statusEntry.filePath) + .call() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/ResetHunkUseCase.kt b/src/main/kotlin/app/git/workspace/ResetHunkUseCase.kt new file mode 100644 index 0000000..e09682c --- /dev/null +++ b/src/main/kotlin/app/git/workspace/ResetHunkUseCase.kt @@ -0,0 +1,62 @@ +package app.git.workspace + +import app.extensions.lineDelimiter +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.diff.DiffEntry +import java.io.File +import java.io.FileWriter +import javax.inject.Inject + +class ResetHunkUseCase @Inject constructor( + private val getLinesFromTextUseCase: GetLinesFromTextUseCase, +) { + suspend operator fun invoke(git: Git, diffEntry: DiffEntry, hunk: Hunk) = withContext(Dispatchers.IO) { + val repository = git.repository + + try { + val file = File(repository.directory.parent, diffEntry.oldPath) + + val content = file.readText() + val textLines = getLinesFromTextUseCase(content, content.lineDelimiter).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("") + + + FileWriter(file).use { fw -> + fw.write(stagedFileText) + } + } catch (ex: Exception) { + throw Exception("Discard hunk failed. Check if the file still exists and has the write permissions set", ex) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/StageAllUseCase.kt b/src/main/kotlin/app/git/workspace/StageAllUseCase.kt new file mode 100644 index 0000000..0e32c85 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/StageAllUseCase.kt @@ -0,0 +1,21 @@ +package app.git.workspace + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import javax.inject.Inject + +class StageAllUseCase @Inject constructor() { + suspend operator fun invoke(git: Git): Unit = withContext(Dispatchers.IO) { + git + .add() + .addFilepattern(".") + .setUpdate(true) // Modified and deleted files + .call() + git + .add() + .addFilepattern(".") + .setUpdate(false) // For newly added files + .call() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/StageEntryUseCase.kt b/src/main/kotlin/app/git/workspace/StageEntryUseCase.kt new file mode 100644 index 0000000..9751748 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/StageEntryUseCase.kt @@ -0,0 +1,15 @@ +package app.git.workspace + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import javax.inject.Inject + +class StageEntryUseCase @Inject constructor() { + suspend operator fun invoke(git: Git, statusEntry: StatusEntry) = withContext(Dispatchers.IO) { + git.add() + .addFilepattern(statusEntry.filePath) + .setUpdate(statusEntry.statusType == StatusType.REMOVED) + .call() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/StageHunkUseCase.kt b/src/main/kotlin/app/git/workspace/StageHunkUseCase.kt new file mode 100644 index 0000000..bc527a9 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/StageHunkUseCase.kt @@ -0,0 +1,74 @@ +package app.git.workspace + +import app.git.EntryContent +import app.git.RawFileManager +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.diff.DiffEntry +import java.nio.ByteBuffer +import javax.inject.Inject + +class StageHunkUseCase @Inject constructor( + private val rawFileManager: RawFileManager, + private val getLinesFromRawTextUseCase: GetLinesFromRawTextUseCase, +) { + suspend operator fun invoke(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 entryContent = rawFileManager.getRawContent( + repository = git.repository, + side = DiffEntry.Side.OLD, + entry = diffEntry, + oldTreeIterator = null, + newTreeIterator = null + ) + + if (entryContent !is EntryContent.Text) + return@withContext + + val textLines = getLinesFromRawTextUseCase(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() + } + } +} + diff --git a/src/main/kotlin/app/git/workspace/StageUntrackedFileUseCase.kt b/src/main/kotlin/app/git/workspace/StageUntrackedFileUseCase.kt new file mode 100644 index 0000000..4ceb74c --- /dev/null +++ b/src/main/kotlin/app/git/workspace/StageUntrackedFileUseCase.kt @@ -0,0 +1,29 @@ +package app.git.workspace + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.diff.DiffEntry +import javax.inject.Inject + +class StageUntrackedFileUseCase @Inject constructor() { + suspend operator fun invoke(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() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/Status.kt b/src/main/kotlin/app/git/workspace/Status.kt new file mode 100644 index 0000000..c9db0f0 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/Status.kt @@ -0,0 +1,34 @@ +package app.git.workspace + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import app.extensions.* + +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 +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/UnstageAllUseCase.kt b/src/main/kotlin/app/git/workspace/UnstageAllUseCase.kt new file mode 100644 index 0000000..4d6d244 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/UnstageAllUseCase.kt @@ -0,0 +1,14 @@ +package app.git.workspace + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import javax.inject.Inject + +class UnstageAllUseCase @Inject constructor() { + suspend operator fun invoke(git: Git): Unit = withContext(Dispatchers.IO) { + git + .reset() + .call() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/UnstageEntryUseCase.kt b/src/main/kotlin/app/git/workspace/UnstageEntryUseCase.kt new file mode 100644 index 0000000..cd74357 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/UnstageEntryUseCase.kt @@ -0,0 +1,15 @@ +package app.git.workspace + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Ref +import javax.inject.Inject + +class UnstageEntryUseCase @Inject constructor() { + suspend operator fun invoke(git: Git, statusEntry: StatusEntry): Ref = withContext(Dispatchers.IO) { + git.reset() + .addPath(statusEntry.filePath) + .call() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/workspace/UnstageHunkUseCase.kt b/src/main/kotlin/app/git/workspace/UnstageHunkUseCase.kt new file mode 100644 index 0000000..4ab1091 --- /dev/null +++ b/src/main/kotlin/app/git/workspace/UnstageHunkUseCase.kt @@ -0,0 +1,73 @@ +package app.git.workspace + +import app.git.EntryContent +import app.git.RawFileManager +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.diff.DiffEntry +import java.nio.ByteBuffer +import javax.inject.Inject + +class UnstageHunkUseCase @Inject constructor( + private val rawFileManager: RawFileManager, + private val getLinesFromRawTextUseCase: GetLinesFromRawTextUseCase +) { + suspend operator fun invoke(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 entryContent = rawFileManager.getRawContent( + repository = git.repository, + side = DiffEntry.Side.OLD, + entry = diffEntry, + oldTreeIterator = null, + newTreeIterator = null + ) + + if (entryContent !is EntryContent.Text) + return@withContext + + val textLines = getLinesFromRawTextUseCase(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() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/ui/UncommitedChanges.kt b/src/main/kotlin/app/ui/UncommitedChanges.kt index 9896f91..1571ff6 100644 --- a/src/main/kotlin/app/ui/UncommitedChanges.kt +++ b/src/main/kotlin/app/ui/UncommitedChanges.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import app.extensions.* import app.git.DiffEntryType -import app.git.StatusEntry -import app.git.StatusType +import app.git.workspace.StatusEntry +import app.git.workspace.StatusType import app.keybindings.KeybindingOption import app.keybindings.matchesBinding import app.theme.* diff --git a/src/main/kotlin/app/ui/context_menu/StagedEntriesContextMenu.kt b/src/main/kotlin/app/ui/context_menu/StagedEntriesContextMenu.kt index 4afa44a..f5c3874 100644 --- a/src/main/kotlin/app/ui/context_menu/StagedEntriesContextMenu.kt +++ b/src/main/kotlin/app/ui/context_menu/StagedEntriesContextMenu.kt @@ -2,8 +2,8 @@ package app.ui.context_menu import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ExperimentalFoundationApi -import app.git.StatusEntry -import app.git.StatusType +import app.git.workspace.StatusEntry +import app.git.workspace.StatusType @OptIn(ExperimentalFoundationApi::class) fun stagedEntriesContextMenuItems( diff --git a/src/main/kotlin/app/ui/context_menu/StatusEntriesContextMenu.kt b/src/main/kotlin/app/ui/context_menu/StatusEntriesContextMenu.kt index 6a0592c..c7aac26 100644 --- a/src/main/kotlin/app/ui/context_menu/StatusEntriesContextMenu.kt +++ b/src/main/kotlin/app/ui/context_menu/StatusEntriesContextMenu.kt @@ -2,8 +2,8 @@ package app.ui.context_menu import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ExperimentalFoundationApi -import app.git.StatusEntry -import app.git.StatusType +import app.git.workspace.StatusEntry +import app.git.workspace.StatusType @OptIn(ExperimentalFoundationApi::class) fun statusEntriesContextMenuItems( diff --git a/src/main/kotlin/app/ui/diff/Diff.kt b/src/main/kotlin/app/ui/diff/Diff.kt index 22593fb..2175dbf 100644 --- a/src/main/kotlin/app/ui/diff/Diff.kt +++ b/src/main/kotlin/app/ui/diff/Diff.kt @@ -36,6 +36,8 @@ import app.git.diff.DiffResult import app.git.diff.Hunk import app.git.diff.Line import app.git.diff.LineType +import app.git.workspace.StatusEntry +import app.git.workspace.StatusType import app.keybindings.KeybindingOption import app.keybindings.matchesBinding import app.theme.* diff --git a/src/main/kotlin/app/ui/log/Log.kt b/src/main/kotlin/app/ui/log/Log.kt index 132f7b1..3d339c4 100644 --- a/src/main/kotlin/app/ui/log/Log.kt +++ b/src/main/kotlin/app/ui/log/Log.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.PathBuilder import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type @@ -40,7 +39,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import app.extensions.* -import app.git.StatusSummary +import app.git.workspace.StatusSummary import app.git.graph.GraphCommitList import app.git.graph.GraphNode import app.keybindings.KeybindingOption @@ -58,7 +57,6 @@ import app.ui.dialogs.ResetBranchDialog import app.viewmodels.LogSearch import app.viewmodels.LogStatus import app.viewmodels.LogViewModel -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.RepositoryState diff --git a/src/main/kotlin/app/viewmodels/DiffViewModel.kt b/src/main/kotlin/app/viewmodels/DiffViewModel.kt index 43f716f..718a7e1 100644 --- a/src/main/kotlin/app/viewmodels/DiffViewModel.kt +++ b/src/main/kotlin/app/viewmodels/DiffViewModel.kt @@ -10,6 +10,7 @@ import app.git.diff.FormatDiffUseCase import app.git.diff.Hunk import app.preferences.AppSettings import app.git.diff.GenerateSplitHunkFromDiffResultUseCase +import app.git.workspace.* import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -22,7 +23,11 @@ private const val DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD = 200L class DiffViewModel @Inject constructor( private val tabState: TabState, private val formatDiffUseCase: FormatDiffUseCase, - private val statusManager: StatusManager, + private val stageHunkUseCase: StageHunkUseCase, + private val unstageHunkUseCase: UnstageHunkUseCase, + private val resetHunkUseCase: ResetHunkUseCase, + private val stageEntryUseCase: StageEntryUseCase, + private val unstageEntryUseCase: UnstageEntryUseCase, private val settings: AppSettings, private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase, ) { @@ -115,32 +120,32 @@ class DiffViewModel @Inject constructor( fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> - statusManager.stageHunk(git, diffEntry, hunk) + stageHunkUseCase(git, diffEntry, hunk) } fun resetHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, showError = true, ) { git -> - statusManager.resetHunk(git, diffEntry, hunk) + resetHunkUseCase(git, diffEntry, hunk) } fun unstageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> - statusManager.unstageHunk(git, diffEntry, hunk) + unstageHunkUseCase(git, diffEntry, hunk) } fun stageFile(statusEntry: StatusEntry) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> - statusManager.stage(git, statusEntry) + stageEntryUseCase(git, statusEntry) } fun unstageFile(statusEntry: StatusEntry) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> - statusManager.unstage(git, statusEntry) + unstageEntryUseCase(git, statusEntry) } fun cancelRunningJobs() { diff --git a/src/main/kotlin/app/viewmodels/LogViewModel.kt b/src/main/kotlin/app/viewmodels/LogViewModel.kt index aa991f1..fd138f3 100644 --- a/src/main/kotlin/app/viewmodels/LogViewModel.kt +++ b/src/main/kotlin/app/viewmodels/LogViewModel.kt @@ -10,6 +10,9 @@ import app.git.graph.GraphNode import app.git.remote_operations.DeleteRemoteBranchUseCase import app.git.remote_operations.PullFromSpecificBranchUseCase import app.git.remote_operations.PushToSpecificBranchUseCase +import app.git.workspace.CheckHasUncommitedChangedUseCase +import app.git.workspace.GetStatusSummaryUseCase +import app.git.workspace.StatusSummary import app.preferences.AppSettings import app.ui.SelectedItem import app.ui.log.LogDialog @@ -37,7 +40,8 @@ private const val LOG_MIN_TIME_IN_MS_TO_SHOW_LOAD = 500L class LogViewModel @Inject constructor( private val logManager: LogManager, - private val statusManager: StatusManager, + private val getStatusSummaryUseCase: GetStatusSummaryUseCase, + private val checkHasUncommitedChangedUseCase: CheckHasUncommitedChangedUseCase, private val getCurrentBranchUseCase: GetCurrentBranchUseCase, private val checkoutRefUseCase: CheckoutRefUseCase, private val createBranchOnCommitUseCase: CreateBranchOnCommitUseCase, @@ -94,7 +98,7 @@ class LogViewModel @Inject constructor( ) { val currentBranch = getCurrentBranchUseCase(git) - val statusSummary = statusManager.getStatusSummary( + val statusSummary = getStatusSummaryUseCase( git = git, ) @@ -206,10 +210,10 @@ class LogViewModel @Inject constructor( private suspend fun uncommitedChangesLoadLog(git: Git) { val currentBranch = getCurrentBranchUseCase(git) - val hasUncommitedChanges = statusManager.hasUncommitedChanges(git) + val hasUncommitedChanges = checkHasUncommitedChangedUseCase(git) val statsSummary = if (hasUncommitedChanges) { - statusManager.getStatusSummary( + getStatusSummaryUseCase( git = git, ) } else diff --git a/src/main/kotlin/app/viewmodels/MenuViewModel.kt b/src/main/kotlin/app/viewmodels/MenuViewModel.kt index 0ee09e4..dab9adb 100644 --- a/src/main/kotlin/app/viewmodels/MenuViewModel.kt +++ b/src/main/kotlin/app/viewmodels/MenuViewModel.kt @@ -5,6 +5,7 @@ import app.git.remote_operations.DeleteRemoteBranchUseCase import app.git.remote_operations.FetchAllBranchesUseCase import app.git.remote_operations.PullBranchUseCase import app.git.remote_operations.PushBranchUseCase +import app.git.workspace.StageUntrackedFileUseCase import java.awt.Desktop import javax.inject.Inject @@ -14,7 +15,7 @@ class MenuViewModel @Inject constructor( private val pushBranchUseCase: PushBranchUseCase, private val fetchAllBranchesUseCase: FetchAllBranchesUseCase, private val stashManager: StashManager, - private val statusManager: StatusManager, + private val stageUntrackedFileUseCase: StageUntrackedFileUseCase, ) { fun pull(rebase: Boolean = false) = tabState.safeProcessing( refreshType = RefreshType.ALL_DATA, @@ -40,14 +41,14 @@ class MenuViewModel @Inject constructor( fun stash() = tabState.safeProcessing( refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG, ) { git -> - statusManager.stageUntrackedFiles(git) + stageUntrackedFileUseCase(git) stashManager.stash(git, null) } fun stashWithMessage(message: String) = tabState.safeProcessing( refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG, ) { git -> - statusManager.stageUntrackedFiles(git) + stageUntrackedFileUseCase(git) stashManager.stash(git, message) } diff --git a/src/main/kotlin/app/viewmodels/StatusViewModel.kt b/src/main/kotlin/app/viewmodels/StatusViewModel.kt index 925ad52..d6fc0a9 100644 --- a/src/main/kotlin/app/viewmodels/StatusViewModel.kt +++ b/src/main/kotlin/app/viewmodels/StatusViewModel.kt @@ -5,6 +5,7 @@ import app.extensions.delayedStateChange import app.extensions.isMerging import app.extensions.isReverting import app.git.* +import app.git.workspace.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -20,10 +21,19 @@ private const val MIN_TIME_IN_MS_TO_SHOW_LOAD = 500L class StatusViewModel @Inject constructor( private val tabState: TabState, - private val statusManager: StatusManager, + private val stageEntryUseCase: StageEntryUseCase, + private val unstageEntryUseCase: UnstageEntryUseCase, + private val resetEntryUseCase: ResetEntryUseCase, + private val stageAllUseCase: StageAllUseCase, + private val unstageAllUseCase: UnstageAllUseCase, private val rebaseManager: RebaseManager, private val mergeManager: MergeManager, private val logManager: LogManager, + private val getStatusUseCase: GetStatusUseCase, + private val getStagedUseCase: GetStagedUseCase, + private val getUnstagedUseCase: GetUnstagedUseCase, + private val checkHasUncommitedChangedUseCase: CheckHasUncommitedChangedUseCase, + private val doCommitUseCase: DoCommitUseCase, ) { private val _stageStatus = MutableStateFlow(StageStatus.Loaded(listOf(), listOf(), false)) val stageStatus: StateFlow = _stageStatus @@ -61,38 +71,38 @@ class StatusViewModel @Inject constructor( fun stage(statusEntry: StatusEntry) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> - statusManager.stage(git, statusEntry) + stageEntryUseCase(git, statusEntry) } fun unstage(statusEntry: StatusEntry) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> - statusManager.unstage(git, statusEntry) + unstageEntryUseCase(git, statusEntry) } fun unstageAll() = tabState.safeProcessing( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> - statusManager.unstageAll(git) + unstageAllUseCase(git) } fun stageAll() = tabState.safeProcessing( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> - statusManager.stageAll(git) + stageAllUseCase(git) } fun resetStaged(statusEntry: StatusEntry) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> - statusManager.reset(git, statusEntry, staged = true) + resetEntryUseCase(git, statusEntry, staged = true) } fun resetUnstaged(statusEntry: StatusEntry) = tabState.runOperation( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> - statusManager.reset(git, statusEntry, staged = false) + resetEntryUseCase(git, statusEntry, staged = false) } private suspend fun loadStatus(git: Git) { @@ -124,9 +134,9 @@ class StatusViewModel @Inject constructor( } } ) { - val status = statusManager.getStatus(git) - val staged = statusManager.getStaged(status).sortedBy { it.filePath } - val unstaged = statusManager.getUnstaged(status).sortedBy { it.filePath } + val status = getStatusUseCase(git) + val staged = getStagedUseCase(status).sortedBy { it.filePath } + val unstaged = getUnstagedUseCase(status).sortedBy { it.filePath } _stageStatus.value = StageStatus.Loaded(staged, unstaged, isPartiallyReloading = false) } @@ -152,7 +162,7 @@ class StatusViewModel @Inject constructor( } private suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) { - lastUncommitedChangesState = statusManager.hasUncommitedChanges(git) + lastUncommitedChangesState = checkHasUncommitedChangedUseCase(git) } fun commit(message: String, amend: Boolean) = tabState.safeProcessing( @@ -163,7 +173,7 @@ class StatusViewModel @Inject constructor( } else message - statusManager.commit(git, commitMessage, amend) + doCommitUseCase(git, commitMessage, amend) updateCommitMessage("") }