diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/rebase/ResumeRebaseInteractiveUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/rebase/ResumeRebaseInteractiveUseCase.kt index 60332da..ab55595 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/rebase/ResumeRebaseInteractiveUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/rebase/ResumeRebaseInteractiveUseCase.kt @@ -16,8 +16,14 @@ class ResumeRebaseInteractiveUseCase @Inject constructor() { .setOperation(RebaseCommand.Operation.PROCESS_STEPS) .call() - if (rebaseResult.status == RebaseResult.Status.FAILED) { - throw UncommitedChangesDetectedException("Rebase interactive failed.") + + when (rebaseResult.status) { + RebaseResult.Status.FAILED -> throw UncommitedChangesDetectedException("Rebase interactive failed.") + RebaseResult.Status.UNCOMMITTED_CHANGES, RebaseResult.Status.CONFLICTS -> throw UncommitedChangesDetectedException( + "You can't have uncommited changes before starting a rebase interactive" + ) + + else -> {} } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/rebase/StartRebaseInteractiveUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/rebase/StartRebaseInteractiveUseCase.kt index a08e47c..3be3ad1 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/rebase/StartRebaseInteractiveUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/rebase/StartRebaseInteractiveUseCase.kt @@ -1,19 +1,30 @@ package com.jetpackduba.gitnuro.git.rebase import com.jetpackduba.gitnuro.exceptions.UncommitedChangesDetectedException +import com.jetpackduba.gitnuro.logging.printDebug import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.RebaseCommand import org.eclipse.jgit.api.RebaseResult +import org.eclipse.jgit.lib.RebaseTodoLine import org.eclipse.jgit.revwalk.RevCommit +import java.io.File import javax.inject.Inject +private const val GIT_REBASE_TODO = "git-rebase-todo" +private const val TAG = "StartRebaseInteractiveU" + class StartRebaseInteractiveUseCase @Inject constructor() { - suspend operator fun invoke(git: Git, interactiveHandler: RebaseCommand.InteractiveHandler, commit: RevCommit) = + suspend operator fun invoke( + git: Git, + interactiveHandler: RebaseCommand.InteractiveHandler, + commit: RevCommit, + stop: Boolean + ): List = withContext(Dispatchers.IO) { val rebaseResult = git.rebase() - .runInteractively(interactiveHandler) + .runInteractively(interactiveHandler, stop) .setOperation(RebaseCommand.Operation.BEGIN) .setUpstream(commit) .call() @@ -26,5 +37,12 @@ class StartRebaseInteractiveUseCase @Inject constructor() { else -> {} } + + val repository = git.repository + val lines = repository.readRebaseTodo("${RebaseCommand.REBASE_MERGE}/$GIT_REBASE_TODO", false) + + printDebug(TAG, "There are ${lines.count()} lines") + + return@withContext lines } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RebaseInteractive.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RebaseInteractive.kt index 6e0a74e..bb53ccb 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RebaseInteractive.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RebaseInteractive.kt @@ -15,8 +15,10 @@ import com.jetpackduba.gitnuro.AppIcons import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField import com.jetpackduba.gitnuro.ui.components.PrimaryButton import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn +import com.jetpackduba.gitnuro.viewmodels.RebaseAction import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveState import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewModel +import com.jetpackduba.gitnuro.viewmodels.RebaseLine import org.eclipse.jgit.lib.RebaseTodoLine import org.eclipse.jgit.lib.RebaseTodoLine.Action @@ -99,7 +101,7 @@ fun RebaseStateLoaded( ) PrimaryButton( modifier = Modifier.padding(end = 16.dp), - enabled = stepsList.any { it.action != Action.PICK }, + enabled = stepsList.any { it.rebaseAction != RebaseAction.PICK }, onClick = { rebaseInteractiveViewModel.continueRebaseInteractive() }, @@ -111,15 +113,15 @@ fun RebaseStateLoaded( @Composable fun RebaseCommit( - rebaseLine: RebaseTodoLine, + rebaseLine: RebaseLine, isFirst: Boolean, message: String?, - onActionChanged: (Action) -> Unit, + onActionChanged: (RebaseAction) -> Unit, onMessageChanged: (String) -> Unit, ) { - val action = rebaseLine.action + val action = rebaseLine.rebaseAction var newMessage by remember(rebaseLine.commit.name(), action) { - if (action == Action.REWORD) { + if (action == RebaseAction.REWORD) { mutableStateOf(message ?: rebaseLine.shortMessage) /* if reword, use the value from the map (if possible)*/ } else mutableStateOf(rebaseLine.shortMessage) // If it's not reword, use the original shortMessage @@ -132,7 +134,7 @@ fun RebaseCommit( .fillMaxWidth() ) { ActionDropdown( - rebaseLine.action, + action, isFirst = isFirst, onActionChanged = onActionChanged, ) @@ -141,14 +143,14 @@ fun RebaseCommit( modifier = Modifier .weight(1f) .heightIn(min = 40.dp), - enabled = rebaseLine.action == Action.REWORD, + enabled = action == RebaseAction.REWORD, value = newMessage, onValueChange = { newMessage = it onMessageChanged(it) }, textStyle = MaterialTheme.typography.body2, - backgroundColor = if (rebaseLine.action == Action.REWORD) { + backgroundColor = if (action == RebaseAction.REWORD) { MaterialTheme.colors.background } else MaterialTheme.colors.surface @@ -160,9 +162,9 @@ fun RebaseCommit( @Composable fun ActionDropdown( - action: Action, + action: RebaseAction, isFirst: Boolean, - onActionChanged: (Action) -> Unit, + onActionChanged: (RebaseAction) -> Unit, ) { var showDropDownMenu by remember { mutableStateOf(false) } Box { @@ -174,7 +176,7 @@ fun ActionDropdown( .padding(end = 8.dp), ) { Text( - action.toToken().replaceFirstChar { it.uppercase() }, + action.displayName, color = MaterialTheme.colors.onBackground, style = MaterialTheme.typography.body1, modifier = Modifier.weight(1f) @@ -206,7 +208,7 @@ fun ActionDropdown( } ) { Text( - text = dropDownOption.toToken().replaceFirstChar { it.uppercase() }, + text = dropDownOption.displayName, style = MaterialTheme.typography.body1, ) } @@ -216,15 +218,15 @@ fun ActionDropdown( } val firstItemActions = listOf( - Action.PICK, - Action.REWORD, + RebaseAction.PICK, + RebaseAction.REWORD, ) val actions = listOf( - Action.PICK, - Action.REWORD, - Action.SQUASH, - Action.FIXUP, -) - - + RebaseAction.PICK, + RebaseAction.REWORD, + RebaseAction.SQUASH, + RebaseAction.FIXUP, + RebaseAction.EDIT, + RebaseAction.DROP, +) \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RebaseInteractiveViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RebaseInteractiveViewModel.kt index 1b55e68..a332dca 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RebaseInteractiveViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RebaseInteractiveViewModel.kt @@ -10,8 +10,6 @@ import com.jetpackduba.gitnuro.git.rebase.ResumeRebaseInteractiveUseCase import com.jetpackduba.gitnuro.git.rebase.StartRebaseInteractiveUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler import org.eclipse.jgit.lib.AbbreviatedObjectId import org.eclipse.jgit.lib.RebaseTodoLine @@ -28,64 +26,69 @@ class RebaseInteractiveViewModel @Inject constructor( private val abortRebaseUseCase: AbortRebaseUseCase, private val resumeRebaseInteractiveUseCase: ResumeRebaseInteractiveUseCase, ) { - private val rebaseInteractiveMutex = Mutex(true) + private lateinit var commit: RevCommit private val _rebaseState = MutableStateFlow(RebaseInteractiveState.Loading) val rebaseState: StateFlow = _rebaseState - var rewordSteps = ArrayDeque() + var rewordSteps = ArrayDeque() - private var cancelled = false - private var completed = false - - private var interactiveHandler = object : InteractiveHandler { - override fun prepareSteps(steps: MutableList) = runBlocking { - println("prepareSteps started") - tabState.refreshData(RefreshType.REPO_STATE) - - val messages = getRebaseLinesFullMessageUseCase(tabState.git, steps) - - _rebaseState.value = RebaseInteractiveState.Loaded(steps, messages) - - println("prepareSteps mutex lock") - rebaseInteractiveMutex.lock() - - if (cancelled) { - throw RebaseCancelledException("Rebase cancelled due to user request") - } + var onRebaseComplete: () -> Unit = {} + private var interactiveHandlerContinue = object : InteractiveHandler { + override fun prepareSteps(steps: MutableList) { val rebaseState = _rebaseState.value if (rebaseState !is RebaseInteractiveState.Loaded) { throw Exception("prepareSteps called when rebaseState is not Loaded") // Should never happen, just in case } - val newSteps = rebaseState.stepsList - rewordSteps = ArrayDeque(newSteps.filter { it.action == Action.REWORD }) + val newSteps = rebaseState.stepsList.toMutableList() + rewordSteps = ArrayDeque(newSteps.filter { it.rebaseAction == RebaseAction.REWORD }) + + val newRebaseTodoLines = newSteps + .filter { it.rebaseAction != RebaseAction.DROP } // Remove dropped lines + .map { it.toRebaseTodoLine() } steps.clear() - steps.addAll(newSteps) - println("prepareSteps finished") + steps.addAll(newRebaseTodoLines) } - override fun modifyCommitMessage(commit: String): String = runBlocking { + override fun modifyCommitMessage(commit: String): String { // This can be called when there aren't any reword steps if squash is used. - val step = rewordSteps.removeLastOrNull() ?: return@runBlocking commit + val step = rewordSteps.removeLastOrNull() ?: return commit val rebaseState = _rebaseState.value if (rebaseState !is RebaseInteractiveState.Loaded) { throw Exception("modifyCommitMessage called when rebaseState is not Loaded") // Should never happen, just in case } - return@runBlocking rebaseState.messages[step.commit.name()] + return rebaseState.messages[step.commit.name()] ?: throw InvalidMessageException("Message for commit $commit is unexpectedly null") } } - suspend fun startRebaseInteractive(revCommit: RevCommit) = tabState.runOperation( + suspend fun startRebaseInteractive(revCommit: RevCommit) = tabState.safeProcessing( refreshType = RefreshType.ALL_DATA, showError = true ) { git -> + this@RebaseInteractiveViewModel.commit = revCommit + + val interactiveHandler = object : InteractiveHandler { + override fun prepareSteps(steps: MutableList?) {} + override fun modifyCommitMessage(message: String?): String = "" + } + try { - startRebaseInteractiveUseCase(git, interactiveHandler, revCommit) - completed = true + val lines = startRebaseInteractiveUseCase(git, interactiveHandler, revCommit, true) + val messages = getRebaseLinesFullMessageUseCase(tabState.git, lines) + val rebaseLines = lines.map { + RebaseLine( + it.action.toRebaseAction(), + it.commit, + it.shortMessage, + ) + } + + _rebaseState.value = RebaseInteractiveState.Loaded(rebaseLines, messages) + } catch (ex: Exception) { if (ex is RebaseCancelledException) { println("Rebase cancelled") @@ -96,10 +99,14 @@ class RebaseInteractiveViewModel @Inject constructor( } } - fun continueRebaseInteractive() = tabState.runOperation( - refreshType = RefreshType.ONLY_LOG, - ) { - rebaseInteractiveMutex.unlock() + fun continueRebaseInteractive() = tabState.safeProcessing( + refreshType = RefreshType.ALL_DATA, + ) { git -> + try { + resumeRebaseInteractiveUseCase(git, interactiveHandlerContinue) + } finally { + onRebaseComplete() + } } fun onCommitMessageChanged(commit: AbbreviatedObjectId, message: String) { @@ -114,7 +121,7 @@ class RebaseInteractiveViewModel @Inject constructor( _rebaseState.value = rebaseState.copy(messages = messagesMap) } - fun onCommitActionChanged(commit: AbbreviatedObjectId, action: Action) { + fun onCommitActionChanged(commit: AbbreviatedObjectId, rebaseAction: RebaseAction) { val rebaseState = _rebaseState.value if (rebaseState !is RebaseInteractiveState.Loaded) @@ -129,7 +136,12 @@ class RebaseInteractiveViewModel @Inject constructor( if (stepIndex >= 0) { val step = newStepsList[stepIndex] - val newTodoLine = RebaseTodoLine(action, step.commit, step.shortMessage) + val newTodoLine = RebaseLine( + rebaseAction, + step.commit, + step.shortMessage + ) + newStepsList[stepIndex] = newTodoLine _rebaseState.value = rebaseState.copy(stepsList = newStepsList) @@ -139,36 +151,60 @@ class RebaseInteractiveViewModel @Inject constructor( fun cancel() = tabState.runOperation( refreshType = RefreshType.REPO_STATE ) { git -> - if (!cancelled && !completed) { - abortRebaseUseCase(git) - - cancelled = true - - rebaseInteractiveMutex.unlock() - } - } - - fun resumeRebase() = tabState.runOperation( - showError = true, - refreshType = RefreshType.NONE, - ) { git -> - try { - resumeRebaseInteractiveUseCase(git, interactiveHandler) - completed = true - } catch (ex: Exception) { - if (ex is RebaseCancelledException) { - println("Rebase cancelled") - } else { - cancel() - throw ex - } - } + abortRebaseUseCase(git) } } sealed interface RebaseInteractiveState { object Loading : RebaseInteractiveState - data class Loaded(val stepsList: List, val messages: Map) : RebaseInteractiveState + data class Loaded(val stepsList: List, val messages: Map) : RebaseInteractiveState data class Failed(val error: String) : RebaseInteractiveState +} + +data class RebaseLine( + val rebaseAction: RebaseAction, + val commit: AbbreviatedObjectId, + val shortMessage: String +) { + fun toRebaseTodoLine(): RebaseTodoLine { + return RebaseTodoLine( + rebaseAction.toAction(), + commit, + shortMessage + ) + } +} + +enum class RebaseAction(val displayName: String) { + PICK("Pick"), + REWORD("Reword"), + SQUASH("Squash"), + FIXUP("Fixup"), + EDIT("Edit"), + DROP("Drop"), + COMMENT("Comment"); + + fun toAction(): Action { + return when (this) { + PICK -> Action.PICK + REWORD -> Action.REWORD + SQUASH -> Action.SQUASH + FIXUP -> Action.FIXUP + EDIT -> Action.EDIT + COMMENT -> Action.COMMENT + DROP -> throw NotImplementedError("To action should not be called when the RebaseAction is DROP") + } + } +} + +fun Action.toRebaseAction(): RebaseAction { + return when (this) { + Action.PICK -> RebaseAction.PICK + Action.REWORD -> RebaseAction.REWORD + Action.EDIT -> RebaseAction.EDIT + Action.SQUASH -> RebaseAction.SQUASH + Action.FIXUP -> RebaseAction.FIXUP + Action.COMMENT -> RebaseAction.COMMENT + } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt index 27aecff..8ecc9c5 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt @@ -165,6 +165,9 @@ class TabViewModel @Inject constructor( private suspend fun onRebaseInteractive(taskEvent: TaskEvent.RebaseInteractive) { rebaseInteractiveViewModel = rebaseInteractiveViewModelProvider.get() rebaseInteractiveViewModel?.startRebaseInteractive(taskEvent.revCommit) + rebaseInteractiveViewModel?.onRebaseComplete = { + rebaseInteractiveViewModel = null + } } /**