package com.jetpackduba.gitnuro.viewmodels import com.jetpackduba.gitnuro.exceptions.InvalidMessageException import com.jetpackduba.gitnuro.exceptions.RebaseCancelledException import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.rebase.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler import org.eclipse.jgit.lib.AbbreviatedObjectId import org.eclipse.jgit.lib.RebaseTodoLine import org.eclipse.jgit.lib.RebaseTodoLine.Action import org.eclipse.jgit.revwalk.RevCommit import javax.inject.Inject private const val TAG = "RebaseInteractiveViewMo" class RebaseInteractiveViewModel @Inject constructor( private val tabState: TabState, private val getRebaseLinesFullMessageUseCase: GetRebaseLinesFullMessageUseCase, private val getRebaseInteractiveTodoLinesUseCase: GetRebaseInteractiveTodoLinesUseCase, private val abortRebaseUseCase: AbortRebaseUseCase, private val resumeRebaseInteractiveUseCase: ResumeRebaseInteractiveUseCase, ) { private lateinit var commit: RevCommit private val _rebaseState = MutableStateFlow(RebaseInteractiveViewState.Loading) val rebaseState: StateFlow = _rebaseState var rewordSteps = ArrayDeque() init { loadRebaseInteractiveData() } private var interactiveHandlerContinue = object : InteractiveHandler { override fun prepareSteps(steps: MutableList) { val rebaseState = _rebaseState.value if (rebaseState !is RebaseInteractiveViewState.Loaded) { throw Exception("prepareSteps called when rebaseState is not Loaded") // Should never happen, just in case } 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(newRebaseTodoLines) } 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 commit val rebaseState = _rebaseState.value if (rebaseState !is RebaseInteractiveViewState.Loaded) { throw Exception("modifyCommitMessage called when rebaseState is not Loaded") // Should never happen, just in case } return rebaseState.messages[step.commit.name()] ?: throw InvalidMessageException("Message for commit $commit is unexpectedly null") } } private fun loadRebaseInteractiveData() = tabState.safeProcessing( refreshType = RefreshType.NONE, ) { git -> try { val lines = getRebaseInteractiveTodoLinesUseCase(git) val messages = getRebaseLinesFullMessageUseCase(tabState.git, lines) val rebaseLines = lines.map { RebaseLine( it.action.toRebaseAction(), it.commit, it.shortMessage, ) } _rebaseState.value = RebaseInteractiveViewState.Loaded(rebaseLines, messages) } catch (ex: Exception) { if (ex is RebaseCancelledException) { println("Rebase cancelled") } else { cancel() throw ex } } } fun continueRebaseInteractive() = tabState.safeProcessing( refreshType = RefreshType.ALL_DATA, ) { git -> resumeRebaseInteractiveUseCase(git, interactiveHandlerContinue) } fun onCommitMessageChanged(commit: AbbreviatedObjectId, message: String) { val rebaseState = _rebaseState.value if (rebaseState !is RebaseInteractiveViewState.Loaded) return val messagesMap = rebaseState.messages.toMutableMap() messagesMap[commit.name()] = message _rebaseState.value = rebaseState.copy(messages = messagesMap) } fun onCommitActionChanged(commit: AbbreviatedObjectId, rebaseAction: RebaseAction) { val rebaseState = _rebaseState.value if (rebaseState !is RebaseInteractiveViewState.Loaded) return val newStepsList = rebaseState.stepsList.toMutableList() // Change the list reference to update the flow with .toList() val stepIndex = newStepsList.indexOfFirst { it.commit == commit } if (stepIndex >= 0) { val step = newStepsList[stepIndex] val newTodoLine = RebaseLine( rebaseAction, step.commit, step.shortMessage ) newStepsList[stepIndex] = newTodoLine _rebaseState.value = rebaseState.copy(stepsList = newStepsList) } } fun cancel() = tabState.runOperation( refreshType = RefreshType.ALL_DATA, ) { git -> abortRebaseUseCase(git) } } sealed interface RebaseInteractiveViewState { object Loading : RebaseInteractiveViewState data class Loaded(val stepsList: List, val messages: Map) : RebaseInteractiveViewState data class Failed(val error: String) : RebaseInteractiveViewState } 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 } }