Refactored status manager into use cases

This commit is contained in:
Abdelilah El Aissaoui 2022-08-26 05:35:58 +02:00
parent d3f2b4a23f
commit 3b1486efb6
32 changed files with 651 additions and 475 deletions

View File

@ -6,8 +6,8 @@ import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import app.git.StatusEntry import app.git.workspace.StatusEntry
import app.git.StatusType import app.git.workspace.StatusType
import app.theme.addFile import app.theme.addFile
import app.theme.conflictFile import app.theme.conflictFile
import app.theme.modifyFile import app.theme.modifyFile

View File

@ -2,6 +2,8 @@ package app.git
import app.extensions.filePath import app.extensions.filePath
import app.extensions.toStatusType import app.extensions.toStatusType
import app.git.workspace.StatusEntry
import app.git.workspace.StatusType
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
sealed class DiffEntryType { sealed class DiffEntryType {

View File

@ -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<String> {
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<String> {
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 || 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
}

View File

@ -10,5 +10,4 @@ class GetRepositoryStateUseCase @Inject constructor() {
suspend operator fun invoke(git: Git): RepositoryState = withContext(Dispatchers.IO) { suspend operator fun invoke(git: Git): RepositoryState = withContext(Dispatchers.IO) {
return@withContext git.repository.repositoryState return@withContext git.repository.repositoryState
} }
} }

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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<String> {
val content = rawFile.rawContent.toString(Charsets.UTF_8)//.removeSuffix(rawFile.lineDelimiter)
val lineDelimiter: String? = rawFile.lineDelimiter
return getLinesFromTextUseCase(content, lineDelimiter)
}
}

View File

@ -0,0 +1,27 @@
package app.git.workspace
import javax.inject.Inject
class GetLinesFromTextUseCase @Inject constructor() {
operator fun invoke(content: String, lineDelimiter: String?): List<String> {
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
}
}

View File

@ -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,
)
}
}

View File

@ -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,
)
}
}

View File

@ -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()
}
}

View File

@ -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,
)
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -30,8 +30,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.extensions.* import app.extensions.*
import app.git.DiffEntryType import app.git.DiffEntryType
import app.git.StatusEntry import app.git.workspace.StatusEntry
import app.git.StatusType import app.git.workspace.StatusType
import app.keybindings.KeybindingOption import app.keybindings.KeybindingOption
import app.keybindings.matchesBinding import app.keybindings.matchesBinding
import app.theme.* import app.theme.*

View File

@ -2,8 +2,8 @@ package app.ui.context_menu
import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import app.git.StatusEntry import app.git.workspace.StatusEntry
import app.git.StatusType import app.git.workspace.StatusType
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun stagedEntriesContextMenuItems( fun stagedEntriesContextMenuItems(

View File

@ -2,8 +2,8 @@ package app.ui.context_menu
import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import app.git.StatusEntry import app.git.workspace.StatusEntry
import app.git.StatusType import app.git.workspace.StatusType
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun statusEntriesContextMenuItems( fun statusEntriesContextMenuItems(

View File

@ -36,6 +36,8 @@ import app.git.diff.DiffResult
import app.git.diff.Hunk import app.git.diff.Hunk
import app.git.diff.Line import app.git.diff.Line
import app.git.diff.LineType import app.git.diff.LineType
import app.git.workspace.StatusEntry
import app.git.workspace.StatusType
import app.keybindings.KeybindingOption import app.keybindings.KeybindingOption
import app.keybindings.matchesBinding import app.keybindings.matchesBinding
import app.theme.* import app.theme.*

View File

@ -26,7 +26,6 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.vector.ImageVector 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.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type 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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.extensions.* import app.extensions.*
import app.git.StatusSummary import app.git.workspace.StatusSummary
import app.git.graph.GraphCommitList import app.git.graph.GraphCommitList
import app.git.graph.GraphNode import app.git.graph.GraphNode
import app.keybindings.KeybindingOption import app.keybindings.KeybindingOption
@ -58,7 +57,6 @@ import app.ui.dialogs.ResetBranchDialog
import app.viewmodels.LogSearch import app.viewmodels.LogSearch
import app.viewmodels.LogStatus import app.viewmodels.LogStatus
import app.viewmodels.LogViewModel import app.viewmodels.LogViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.lib.RepositoryState

View File

@ -10,6 +10,7 @@ import app.git.diff.FormatDiffUseCase
import app.git.diff.Hunk import app.git.diff.Hunk
import app.preferences.AppSettings import app.preferences.AppSettings
import app.git.diff.GenerateSplitHunkFromDiffResultUseCase import app.git.diff.GenerateSplitHunkFromDiffResultUseCase
import app.git.workspace.*
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow 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( class DiffViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val formatDiffUseCase: FormatDiffUseCase, 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 settings: AppSettings,
private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase, private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase,
) { ) {
@ -115,32 +120,32 @@ class DiffViewModel @Inject constructor(
fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation( fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.stageHunk(git, diffEntry, hunk) stageHunkUseCase(git, diffEntry, hunk)
} }
fun resetHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation( fun resetHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
showError = true, showError = true,
) { git -> ) { git ->
statusManager.resetHunk(git, diffEntry, hunk) resetHunkUseCase(git, diffEntry, hunk)
} }
fun unstageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation( fun unstageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.unstageHunk(git, diffEntry, hunk) unstageHunkUseCase(git, diffEntry, hunk)
} }
fun stageFile(statusEntry: StatusEntry) = tabState.runOperation( fun stageFile(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.stage(git, statusEntry) stageEntryUseCase(git, statusEntry)
} }
fun unstageFile(statusEntry: StatusEntry) = tabState.runOperation( fun unstageFile(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.unstage(git, statusEntry) unstageEntryUseCase(git, statusEntry)
} }
fun cancelRunningJobs() { fun cancelRunningJobs() {

View File

@ -10,6 +10,9 @@ import app.git.graph.GraphNode
import app.git.remote_operations.DeleteRemoteBranchUseCase import app.git.remote_operations.DeleteRemoteBranchUseCase
import app.git.remote_operations.PullFromSpecificBranchUseCase import app.git.remote_operations.PullFromSpecificBranchUseCase
import app.git.remote_operations.PushToSpecificBranchUseCase 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.preferences.AppSettings
import app.ui.SelectedItem import app.ui.SelectedItem
import app.ui.log.LogDialog 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( class LogViewModel @Inject constructor(
private val logManager: LogManager, private val logManager: LogManager,
private val statusManager: StatusManager, private val getStatusSummaryUseCase: GetStatusSummaryUseCase,
private val checkHasUncommitedChangedUseCase: CheckHasUncommitedChangedUseCase,
private val getCurrentBranchUseCase: GetCurrentBranchUseCase, private val getCurrentBranchUseCase: GetCurrentBranchUseCase,
private val checkoutRefUseCase: CheckoutRefUseCase, private val checkoutRefUseCase: CheckoutRefUseCase,
private val createBranchOnCommitUseCase: CreateBranchOnCommitUseCase, private val createBranchOnCommitUseCase: CreateBranchOnCommitUseCase,
@ -94,7 +98,7 @@ class LogViewModel @Inject constructor(
) { ) {
val currentBranch = getCurrentBranchUseCase(git) val currentBranch = getCurrentBranchUseCase(git)
val statusSummary = statusManager.getStatusSummary( val statusSummary = getStatusSummaryUseCase(
git = git, git = git,
) )
@ -206,10 +210,10 @@ class LogViewModel @Inject constructor(
private suspend fun uncommitedChangesLoadLog(git: Git) { private suspend fun uncommitedChangesLoadLog(git: Git) {
val currentBranch = getCurrentBranchUseCase(git) val currentBranch = getCurrentBranchUseCase(git)
val hasUncommitedChanges = statusManager.hasUncommitedChanges(git) val hasUncommitedChanges = checkHasUncommitedChangedUseCase(git)
val statsSummary = if (hasUncommitedChanges) { val statsSummary = if (hasUncommitedChanges) {
statusManager.getStatusSummary( getStatusSummaryUseCase(
git = git, git = git,
) )
} else } else

View File

@ -5,6 +5,7 @@ import app.git.remote_operations.DeleteRemoteBranchUseCase
import app.git.remote_operations.FetchAllBranchesUseCase import app.git.remote_operations.FetchAllBranchesUseCase
import app.git.remote_operations.PullBranchUseCase import app.git.remote_operations.PullBranchUseCase
import app.git.remote_operations.PushBranchUseCase import app.git.remote_operations.PushBranchUseCase
import app.git.workspace.StageUntrackedFileUseCase
import java.awt.Desktop import java.awt.Desktop
import javax.inject.Inject import javax.inject.Inject
@ -14,7 +15,7 @@ class MenuViewModel @Inject constructor(
private val pushBranchUseCase: PushBranchUseCase, private val pushBranchUseCase: PushBranchUseCase,
private val fetchAllBranchesUseCase: FetchAllBranchesUseCase, private val fetchAllBranchesUseCase: FetchAllBranchesUseCase,
private val stashManager: StashManager, private val stashManager: StashManager,
private val statusManager: StatusManager, private val stageUntrackedFileUseCase: StageUntrackedFileUseCase,
) { ) {
fun pull(rebase: Boolean = false) = tabState.safeProcessing( fun pull(rebase: Boolean = false) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
@ -40,14 +41,14 @@ class MenuViewModel @Inject constructor(
fun stash() = tabState.safeProcessing( fun stash() = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG, refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG,
) { git -> ) { git ->
statusManager.stageUntrackedFiles(git) stageUntrackedFileUseCase(git)
stashManager.stash(git, null) stashManager.stash(git, null)
} }
fun stashWithMessage(message: String) = tabState.safeProcessing( fun stashWithMessage(message: String) = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG, refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG,
) { git -> ) { git ->
statusManager.stageUntrackedFiles(git) stageUntrackedFileUseCase(git)
stashManager.stash(git, message) stashManager.stash(git, message)
} }

View File

@ -5,6 +5,7 @@ import app.extensions.delayedStateChange
import app.extensions.isMerging import app.extensions.isMerging
import app.extensions.isReverting import app.extensions.isReverting
import app.git.* import app.git.*
import app.git.workspace.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -20,10 +21,19 @@ private const val MIN_TIME_IN_MS_TO_SHOW_LOAD = 500L
class StatusViewModel @Inject constructor( class StatusViewModel @Inject constructor(
private val tabState: TabState, 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 rebaseManager: RebaseManager,
private val mergeManager: MergeManager, private val mergeManager: MergeManager,
private val logManager: LogManager, 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>(StageStatus.Loaded(listOf(), listOf(), false)) private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf(), false))
val stageStatus: StateFlow<StageStatus> = _stageStatus val stageStatus: StateFlow<StageStatus> = _stageStatus
@ -61,38 +71,38 @@ class StatusViewModel @Inject constructor(
fun stage(statusEntry: StatusEntry) = tabState.runOperation( fun stage(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.stage(git, statusEntry) stageEntryUseCase(git, statusEntry)
} }
fun unstage(statusEntry: StatusEntry) = tabState.runOperation( fun unstage(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.unstage(git, statusEntry) unstageEntryUseCase(git, statusEntry)
} }
fun unstageAll() = tabState.safeProcessing( fun unstageAll() = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.unstageAll(git) unstageAllUseCase(git)
} }
fun stageAll() = tabState.safeProcessing( fun stageAll() = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.stageAll(git) stageAllUseCase(git)
} }
fun resetStaged(statusEntry: StatusEntry) = tabState.runOperation( fun resetStaged(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.reset(git, statusEntry, staged = true) resetEntryUseCase(git, statusEntry, staged = true)
} }
fun resetUnstaged(statusEntry: StatusEntry) = tabState.runOperation( fun resetUnstaged(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
statusManager.reset(git, statusEntry, staged = false) resetEntryUseCase(git, statusEntry, staged = false)
} }
private suspend fun loadStatus(git: Git) { private suspend fun loadStatus(git: Git) {
@ -124,9 +134,9 @@ class StatusViewModel @Inject constructor(
} }
} }
) { ) {
val status = statusManager.getStatus(git) val status = getStatusUseCase(git)
val staged = statusManager.getStaged(status).sortedBy { it.filePath } val staged = getStagedUseCase(status).sortedBy { it.filePath }
val unstaged = statusManager.getUnstaged(status).sortedBy { it.filePath } val unstaged = getUnstagedUseCase(status).sortedBy { it.filePath }
_stageStatus.value = StageStatus.Loaded(staged, unstaged, isPartiallyReloading = false) _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) { private suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) {
lastUncommitedChangesState = statusManager.hasUncommitedChanges(git) lastUncommitedChangesState = checkHasUncommitedChangedUseCase(git)
} }
fun commit(message: String, amend: Boolean) = tabState.safeProcessing( fun commit(message: String, amend: Boolean) = tabState.safeProcessing(
@ -163,7 +173,7 @@ class StatusViewModel @Inject constructor(
} else } else
message message
statusManager.commit(git, commitMessage, amend) doCommitUseCase(git, commitMessage, amend)
updateCommitMessage("") updateCommitMessage("")
} }