From 51d79cff8fcfd98d65941dee46a27158caa80c29 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Sun, 22 May 2022 00:47:36 +0200 Subject: [PATCH] Reimplemented rebase interactive as a part of the tab instead of a dialog --- .../exceptions/RebaseCancelledException.kt | 3 + src/main/kotlin/app/git/RebaseManager.kt | 45 +++- src/main/kotlin/app/git/TabState.kt | 50 ++++- src/main/kotlin/app/git/TaskEvent.kt | 7 + ...eractiveDialog.kt => RebaseInteractive.kt} | 57 ++--- src/main/kotlin/app/ui/RepositoryOpen.kt | 211 ++++++++++-------- src/main/kotlin/app/ui/log/Log.kt | 9 +- src/main/kotlin/app/ui/log/LogDialog.kt | 1 - .../kotlin/app/viewmodels/LogViewModel.kt | 15 +- .../viewmodels/RebaseInteractiveViewModel.kt | 117 ++++++---- .../kotlin/app/viewmodels/TabViewModel.kt | 63 ++++-- 11 files changed, 355 insertions(+), 223 deletions(-) create mode 100644 src/main/kotlin/app/exceptions/RebaseCancelledException.kt create mode 100644 src/main/kotlin/app/git/TaskEvent.kt rename src/main/kotlin/app/ui/{dialogs/RebaseInteractiveDialog.kt => RebaseInteractive.kt} (79%) diff --git a/src/main/kotlin/app/exceptions/RebaseCancelledException.kt b/src/main/kotlin/app/exceptions/RebaseCancelledException.kt new file mode 100644 index 0000000..8dce0f8 --- /dev/null +++ b/src/main/kotlin/app/exceptions/RebaseCancelledException.kt @@ -0,0 +1,3 @@ +package app.exceptions + +class RebaseCancelledException(msg: String) : GitnuroException(msg) \ No newline at end of file diff --git a/src/main/kotlin/app/git/RebaseManager.kt b/src/main/kotlin/app/git/RebaseManager.kt index 619188c..d706370 100644 --- a/src/main/kotlin/app/git/RebaseManager.kt +++ b/src/main/kotlin/app/git/RebaseManager.kt @@ -7,10 +7,11 @@ import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.RebaseCommand import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler import org.eclipse.jgit.api.RebaseResult +import org.eclipse.jgit.errors.AmbiguousObjectException +import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.RebaseTodoLine import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.revwalk.RevCommit -import org.eclipse.jgit.revwalk.RevCommitList import org.eclipse.jgit.revwalk.RevWalk import javax.inject.Inject @@ -59,20 +60,42 @@ class RebaseManager @Inject constructor( suspend fun rebaseLinesFullMessage( git: Git, rebaseTodoLines: List, - commit: RevCommit ): Map = withContext(Dispatchers.IO) { - val revWalk = RevWalk(git.repository) - markCurrentBranchAsStart(revWalk, git) - val revCommitList = RevCommitList() - revCommitList.source(revWalk) - revCommitList.fillTo(commit, Int.MAX_VALUE) + return@withContext rebaseTodoLines.map { line -> + val commit = getCommitFromLine(git, line) + val fullMessage = commit?.fullMessage ?: line.shortMessage + line.commit.name() to fullMessage + }.toMap() + } - val commitsList = revCommitList.toList() + private fun getCommitFromLine(git: Git, line: RebaseTodoLine): RevCommit? { + val resolvedList: List = try { + listOf(git.repository.resolve("${line.commit.name()}^{commit}")) + } catch (ex: AmbiguousObjectException) { + ex.candidates.toList() + } - return@withContext rebaseTodoLines.associate { rebaseLine -> - val fullMessage = getFullMessage(rebaseLine, commitsList) ?: rebaseLine.shortMessage - rebaseLine.commit.name() to fullMessage + if (resolvedList.isEmpty()) { + println("Commit search failed for line ${line.commit} - ${line.shortMessage}") + return null + } else if (resolvedList.count() == 1) { + val resolvedId = resolvedList.firstOrNull() + + return if (resolvedId == null) + null + else + git.repository.parseCommit(resolvedId) + } else { + println("Multiple matching commits for line ${line.commit} - ${line.shortMessage}") + for (candidateId in resolvedList) { + val candidateCommit = git.repository.parseCommit(candidateId) + if (line.shortMessage == candidateCommit.shortMessage) + return candidateCommit + } + + println("None of the matching commits has a matching short message") + return null } } diff --git a/src/main/kotlin/app/git/TabState.kt b/src/main/kotlin/app/git/TabState.kt index 7ebc5a8..6365c14 100644 --- a/src/main/kotlin/app/git/TabState.kt +++ b/src/main/kotlin/app/git/TabState.kt @@ -5,10 +5,7 @@ import app.di.TabScope import app.newErrorNow import app.ui.SelectedItem import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.eclipse.jgit.api.Git @@ -23,6 +20,8 @@ class TabState @Inject constructor( ) { private val _selectedItem = MutableStateFlow(SelectedItem.None) val selectedItem: StateFlow = _selectedItem + private val _taskEvent = MutableSharedFlow() + val taskEvent: SharedFlow = _taskEvent var git: Git? = null val safeGit: Git @@ -41,7 +40,6 @@ class TabState @Inject constructor( val managerScope = CoroutineScope(SupervisorJob()) - /** * Property that indicates if a git operation is running */ @@ -139,6 +137,43 @@ class TabState @Inject constructor( } } + suspend fun coRunOperation( + showError: Boolean = false, + refreshType: RefreshType, + refreshEvenIfCrashes: Boolean = false, + block: suspend (git: Git) -> Unit + ) = withContext(Dispatchers.IO) { + var hasProcessFailed = false + + operationRunning = true + try { + block(safeGit) + + if (refreshType != RefreshType.NONE) + _refreshData.emit(refreshType) + } catch (ex: Exception) { + ex.printStackTrace() + + hasProcessFailed = true + + if (showError) + errorsManager.addError(newErrorNow(ex, ex.localizedMessage)) + } finally { + launch { + // Add a slight delay because sometimes the file watcher takes a few moments to notify a change in the + // filesystem, therefore notifying late and being operationRunning already false (which leads to a full + // refresh because there have been changes in the git dir). This can be easily triggered by interactive + // rebase. + delay(500) + operationRunning = false + } + + + if (refreshType != RefreshType.NONE && (!hasProcessFailed || refreshEvenIfCrashes)) + _refreshData.emit(refreshType) + } + } + suspend fun refreshData(refreshType: RefreshType) { _refreshData.emit(refreshType) } @@ -169,11 +204,16 @@ class TabState @Inject constructor( fun newSelectedItem(selectedItem: SelectedItem) { _selectedItem.value = selectedItem } + + suspend fun emitNewTaskEvent(taskEvent: TaskEvent) { + _taskEvent.emit(taskEvent) + } } enum class RefreshType { NONE, ALL_DATA, + REPO_STATE, ONLY_LOG, STASHES, UNCOMMITED_CHANGES, diff --git a/src/main/kotlin/app/git/TaskEvent.kt b/src/main/kotlin/app/git/TaskEvent.kt new file mode 100644 index 0000000..f8e5706 --- /dev/null +++ b/src/main/kotlin/app/git/TaskEvent.kt @@ -0,0 +1,7 @@ +package app.git + +import org.eclipse.jgit.revwalk.RevCommit + +sealed interface TaskEvent { + data class RebaseInteractive(val revCommit: RevCommit): TaskEvent +} \ No newline at end of file diff --git a/src/main/kotlin/app/ui/dialogs/RebaseInteractiveDialog.kt b/src/main/kotlin/app/ui/RebaseInteractive.kt similarity index 79% rename from src/main/kotlin/app/ui/dialogs/RebaseInteractiveDialog.kt rename to src/main/kotlin/app/ui/RebaseInteractive.kt index 4de8dd0..b12c46b 100644 --- a/src/main/kotlin/app/ui/dialogs/RebaseInteractiveDialog.kt +++ b/src/main/kotlin/app/ui/RebaseInteractive.kt @@ -3,7 +3,6 @@ package app.ui.dialogs import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.* import androidx.compose.runtime.* @@ -19,43 +18,33 @@ import app.viewmodels.RebaseInteractiveState import app.viewmodels.RebaseInteractiveViewModel import org.eclipse.jgit.lib.RebaseTodoLine import org.eclipse.jgit.lib.RebaseTodoLine.Action -import org.eclipse.jgit.revwalk.RevCommit @OptIn(ExperimentalFoundationApi::class) @Composable -fun RebaseInteractiveDialog( +fun RebaseInteractive( rebaseInteractiveViewModel: RebaseInteractiveViewModel, - revCommit: RevCommit, - onClose: () -> Unit, ) { val rebaseState = rebaseInteractiveViewModel.rebaseState.collectAsState() val rebaseStateValue = rebaseState.value - LaunchedEffect(Unit) { - rebaseInteractiveViewModel.revCommit = revCommit - rebaseInteractiveViewModel.startRebaseInteractive() - } - - MaterialDialog { - - Box( - modifier = Modifier - .background(MaterialTheme.colors.background) - .fillMaxSize(0.8f), - ) { - when (rebaseStateValue) { - is RebaseInteractiveState.Failed -> {} - RebaseInteractiveState.Finished -> onClose() - is RebaseInteractiveState.Loaded -> { - RebaseStateLoaded( - rebaseInteractiveViewModel, - rebaseStateValue, - onCancel = onClose, - ) - } - RebaseInteractiveState.Loading -> { - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) - } + Box( + modifier = Modifier + .background(MaterialTheme.colors.surface) + .fillMaxSize(), + ) { + when (rebaseStateValue) { + is RebaseInteractiveState.Failed -> {} + is RebaseInteractiveState.Loaded -> { + RebaseStateLoaded( + rebaseInteractiveViewModel, + rebaseStateValue, + onCancel = { + rebaseInteractiveViewModel.cancel() + }, + ) + } + RebaseInteractiveState.Loading -> { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } } } @@ -73,7 +62,7 @@ fun RebaseStateLoaded( Text( text = "Rebase interactive", color = MaterialTheme.colors.primaryTextColor, - modifier = Modifier.padding(all = 16.dp) + modifier = Modifier.padding(all = 20.dp) ) ScrollableLazyColumn(modifier = Modifier.weight(1f)) { @@ -102,6 +91,7 @@ fun RebaseStateLoaded( Text("Cancel") } PrimaryButton( + modifier = Modifier.padding(end = 16.dp), onClick = { rebaseInteractiveViewModel.continueRebaseInteractive() }, @@ -120,8 +110,9 @@ fun RebaseCommit( ) { val action = rebaseLine.action var newMessage by remember(rebaseLine.commit.name(), action) { - if(action == Action.REWORD) { - mutableStateOf(message ?: rebaseLine.shortMessage) /* if reword, use the value from the map (if possible)*/ } else + if (action == Action.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 } diff --git a/src/main/kotlin/app/ui/RepositoryOpen.kt b/src/main/kotlin/app/ui/RepositoryOpen.kt index ea8067d..7639be2 100644 --- a/src/main/kotlin/app/ui/RepositoryOpen.kt +++ b/src/main/kotlin/app/ui/RepositoryOpen.kt @@ -4,12 +4,15 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.git.DiffEntryType import app.theme.borderColor +import app.theme.primaryTextColor import app.ui.dialogs.NewBranchDialog +import app.ui.dialogs.RebaseInteractive import app.ui.log.Log import app.viewmodels.TabViewModel import openRepositoryDialog @@ -20,7 +23,7 @@ import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState -@OptIn(ExperimentalSplitPaneApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class) +@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) @Composable fun RepositoryOpenPage(tabViewModel: TabViewModel) { val repositoryState by tabViewModel.repositoryState.collectAsState() @@ -28,9 +31,6 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { val selectedItem by tabViewModel.selectedItem.collectAsState() var showNewBranchDialog by remember { mutableStateOf(false) } -// LaunchedEffect(selectedItem) { -// tabViewModel.newDiffSelected = null -// } if (showNewBranchDialog) { NewBranchDialog( @@ -45,107 +45,130 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { } Column { - Menu( - menuViewModel = tabViewModel.menuViewModel, - onRepositoryOpen = { - openRepositoryDialog(tabViewModel = tabViewModel) - }, - onCreateBranch = { showNewBranchDialog = true } - ) + if (repositoryState == RepositoryState.REBASING_INTERACTIVE) { + val rebaseInteractiveViewModel = tabViewModel.rebaseInteractiveViewModel - Row { - HorizontalSplitPane { - first(minSize = 200.dp) { - Column( - modifier = Modifier - .widthIn(min = 300.dp) - .weight(0.15f) - .fillMaxHeight() - ) { - Branches( - branchesViewModel = tabViewModel.branchesViewModel, - ) - Remotes( - remotesViewModel = tabViewModel.remotesViewModel, - ) - Tags( - tagsViewModel = tabViewModel.tagsViewModel, - ) - Stashes( - stashesViewModel = tabViewModel.stashesViewModel, - ) - } + // TODO Implement continue rebase interactive when gitnuro has been closed + if (rebaseInteractiveViewModel != null) { + RebaseInteractive(rebaseInteractiveViewModel) + } else { + Text("Rebase started externally", color = MaterialTheme.colors.primaryTextColor) + } + } else { + Menu( + menuViewModel = tabViewModel.menuViewModel, + onRepositoryOpen = { + openRepositoryDialog(tabViewModel = tabViewModel) + }, + onCreateBranch = { showNewBranchDialog = true } + ) + + RepoContent(tabViewModel, diffSelected, selectedItem, repositoryState) + } + + } +} + +@OptIn(ExperimentalSplitPaneApi::class) +@Composable +fun RepoContent( + tabViewModel: TabViewModel, + diffSelected: DiffEntryType?, + selectedItem: SelectedItem, + repositoryState: RepositoryState +) { + Row { + HorizontalSplitPane { + first(minSize = 200.dp) { + Column( + modifier = Modifier + .widthIn(min = 300.dp) + .weight(0.15f) + .fillMaxHeight() + ) { + Branches( + branchesViewModel = tabViewModel.branchesViewModel, + ) + Remotes( + remotesViewModel = tabViewModel.remotesViewModel, + ) + Tags( + tagsViewModel = tabViewModel.tagsViewModel, + ) + Stashes( + stashesViewModel = tabViewModel.stashesViewModel, + ) } + } - second { - HorizontalSplitPane( - splitPaneState = rememberSplitPaneState(0.9f) - ) { - first { - Box( - modifier = Modifier - .fillMaxSize() - .border( - width = 2.dp, - color = MaterialTheme.colors.borderColor, - shape = RoundedCornerShape(4.dp) + second { + HorizontalSplitPane( + splitPaneState = rememberSplitPaneState(0.9f) + ) { + first { + Box( + modifier = Modifier + .fillMaxSize() + .border( + width = 2.dp, + color = MaterialTheme.colors.borderColor, + shape = RoundedCornerShape(4.dp) + ) + ) { + when (diffSelected) { + null -> { + Log( + logViewModel = tabViewModel.logViewModel, + selectedItem = selectedItem, + repositoryState = repositoryState, ) - ) { - when (diffSelected) { - null -> { - Log( - logViewModel = tabViewModel.logViewModel, - selectedItem = selectedItem, - repositoryState = repositoryState, - ) - } - else -> { - Diff( - diffViewModel = tabViewModel.diffViewModel, - onCloseDiffView = { tabViewModel.newDiffSelected = null }) - } + } + else -> { + Diff( + diffViewModel = tabViewModel.diffViewModel, + onCloseDiffView = { tabViewModel.newDiffSelected = null }) } } } + } - second(minSize = 300.dp) { - Box( - modifier = Modifier - .fillMaxHeight() - ) { - val safeSelectedItem = selectedItem - if (safeSelectedItem == SelectedItem.UncommitedChanges) { - UncommitedChanges( - statusViewModel = tabViewModel.statusViewModel, - selectedEntryType = diffSelected, - repositoryState = repositoryState, - onStagedDiffEntrySelected = { diffEntry -> - tabViewModel.newDiffSelected = if (diffEntry != null) { - if (repositoryState == RepositoryState.SAFE) - DiffEntryType.SafeStagedDiff(diffEntry) - else - DiffEntryType.UnsafeStagedDiff(diffEntry) - } else { - null - } - }, - onUnstagedDiffEntrySelected = { diffEntry -> + second(minSize = 300.dp) { + Box( + modifier = Modifier + .fillMaxHeight() + ) { + val safeSelectedItem = selectedItem + if (safeSelectedItem == SelectedItem.UncommitedChanges) { + UncommitedChanges( + statusViewModel = tabViewModel.statusViewModel, + selectedEntryType = diffSelected, + repositoryState = repositoryState, + onStagedDiffEntrySelected = { diffEntry -> + tabViewModel.newDiffSelected = if (diffEntry != null) { if (repositoryState == RepositoryState.SAFE) - tabViewModel.newDiffSelected = DiffEntryType.SafeUnstagedDiff(diffEntry) + DiffEntryType.SafeStagedDiff(diffEntry) else - tabViewModel.newDiffSelected = DiffEntryType.UnsafeUnstagedDiff(diffEntry) + DiffEntryType.UnsafeStagedDiff(diffEntry) + } else { + null } - ) - } else if (safeSelectedItem is SelectedItem.CommitBasedItem) { - CommitChanges( - commitChangesViewModel = tabViewModel.commitChangesViewModel, - selectedItem = safeSelectedItem, - diffSelected = diffSelected, - onDiffSelected = { diffEntry -> - tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry) - } - ) - } + }, + onUnstagedDiffEntrySelected = { diffEntry -> + if (repositoryState == RepositoryState.SAFE) + tabViewModel.newDiffSelected = DiffEntryType.SafeUnstagedDiff(diffEntry) + else + tabViewModel.newDiffSelected = DiffEntryType.UnsafeUnstagedDiff(diffEntry) + } + ) + } else if (safeSelectedItem is SelectedItem.CommitBasedItem) { + CommitChanges( + commitChangesViewModel = tabViewModel.commitChangesViewModel, + selectedItem = safeSelectedItem, + diffSelected = diffSelected, + onDiffSelected = { diffEntry -> + tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry) + } + ) } } } diff --git a/src/main/kotlin/app/ui/log/Log.kt b/src/main/kotlin/app/ui/log/Log.kt index d8aed6e..493a99a 100644 --- a/src/main/kotlin/app/ui/log/Log.kt +++ b/src/main/kotlin/app/ui/log/Log.kt @@ -349,7 +349,7 @@ fun MessagesList( resetBranch = { onShowLogDialog(LogDialog.ResetBranch(graphNode)) }, onMergeBranch = { ref -> onShowLogDialog(LogDialog.MergeBranch(ref)) }, onRebaseBranch = { ref -> onShowLogDialog(LogDialog.RebaseBranch(ref)) }, - onRebaseInteractive = { onShowLogDialog(LogDialog.RebaseInteractive(graphNode)) }, + onRebaseInteractive = { logViewModel.rebaseInteractive(graphNode) }, onRevCommitSelected = { logViewModel.selectLogLine(graphNode) }, ) } @@ -481,13 +481,6 @@ fun LogDialogs( }) } } - is LogDialog.RebaseInteractive -> { - RebaseInteractiveDialog( - revCommit = showLogDialog.revCommit, - rebaseInteractiveViewModel = checkNotNull(logViewModel.rebaseInteractiveViewModel), // Never null, value should be set before showing dialog - onClose = onResetShowLogDialog, - ) - } LogDialog.None -> { } } diff --git a/src/main/kotlin/app/ui/log/LogDialog.kt b/src/main/kotlin/app/ui/log/LogDialog.kt index 7292b79..e54f305 100644 --- a/src/main/kotlin/app/ui/log/LogDialog.kt +++ b/src/main/kotlin/app/ui/log/LogDialog.kt @@ -11,5 +11,4 @@ sealed class LogDialog { data class ResetBranch(val graphNode: GraphNode) : LogDialog() data class MergeBranch(val ref: Ref) : LogDialog() data class RebaseBranch(val ref: Ref) : LogDialog() - data class RebaseInteractive(val revCommit: RevCommit) : LogDialog() } \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/LogViewModel.kt b/src/main/kotlin/app/viewmodels/LogViewModel.kt index 5542f66..4c985b8 100644 --- a/src/main/kotlin/app/viewmodels/LogViewModel.kt +++ b/src/main/kotlin/app/viewmodels/LogViewModel.kt @@ -36,13 +36,9 @@ class LogViewModel @Inject constructor( private val mergeManager: MergeManager, private val remoteOperationsManager: RemoteOperationsManager, private val tabState: TabState, - private val rebaseInteractiveViewModelProvider: Provider ) { private val _logStatus = MutableStateFlow(LogStatus.Loading) - var rebaseInteractiveViewModel: RebaseInteractiveViewModel? = null - private set - val logStatus: StateFlow get() = _logStatus @@ -312,17 +308,18 @@ class LogViewModel @Inject constructor( } fun showDialog(dialog: LogDialog) { - rebaseInteractiveViewModel = if(dialog is LogDialog.RebaseInteractive) { - rebaseInteractiveViewModelProvider.get() - } else - null - _logDialog.value = dialog } fun closeSearch() { _logSearchFilterResults.value = LogSearch.NotSearching } + + fun rebaseInteractive(revCommit: RevCommit) = tabState.runOperation ( + refreshType = RefreshType.NONE + ) { + tabState.emitNewTaskEvent(TaskEvent.RebaseInteractive(revCommit)) + } } sealed class LogStatus { diff --git a/src/main/kotlin/app/viewmodels/RebaseInteractiveViewModel.kt b/src/main/kotlin/app/viewmodels/RebaseInteractiveViewModel.kt index ae0de56..442dfca 100644 --- a/src/main/kotlin/app/viewmodels/RebaseInteractiveViewModel.kt +++ b/src/main/kotlin/app/viewmodels/RebaseInteractiveViewModel.kt @@ -1,11 +1,14 @@ package app.viewmodels import app.exceptions.InvalidMessageException +import app.exceptions.RebaseCancelledException import app.git.RebaseManager import app.git.RefreshType import app.git.TabState 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 @@ -17,69 +20,74 @@ class RebaseInteractiveViewModel @Inject constructor( private val tabState: TabState, private val rebaseManager: RebaseManager, ) { - lateinit var revCommit: RevCommit - + private val rebaseInteractiveMutex = Mutex(true) private val _rebaseState = MutableStateFlow(RebaseInteractiveState.Loading) val rebaseState: StateFlow = _rebaseState + var rewordSteps = ArrayDeque() + + private var cancelled = false + private var completed = false private var interactiveHandler = object : InteractiveHandler { - override fun prepareSteps(steps: MutableList?) { - _rebaseState.value = RebaseInteractiveState.Loaded(steps?.reversed() ?: emptyList(), emptyMap()) + override fun prepareSteps(steps: MutableList) = runBlocking { + println("prepareSteps started") + tabState.refreshData(RefreshType.REPO_STATE) + + tabState.coRunOperation(refreshType = RefreshType.NONE) { git -> + val messages = rebaseManager.rebaseLinesFullMessage(git, steps) + + _rebaseState.value = RebaseInteractiveState.Loaded(steps, messages) + } + + println("prepareSteps mutex lock") + rebaseInteractiveMutex.lock() + + if (cancelled) { + throw RebaseCancelledException("Rebase cancelled due to user request") + } + + 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 }) + + steps.clear() + steps.addAll(newSteps) + println("prepareSteps finished") } - override fun modifyCommitMessage(commit: String?): String { - return commit.orEmpty() // we don't care about this since it's not called + override fun modifyCommitMessage(commit: String): String = runBlocking { + // This can be called when there aren't any reword steps if squash is used. + val step = rewordSteps.removeLastOrNull() ?: return@runBlocking 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()] + ?: throw InvalidMessageException("Message for commit $commit is unexpectedly null") } } - fun startRebaseInteractive() = tabState.runOperation( - refreshType = RefreshType.NONE, + suspend fun startRebaseInteractive(revCommit: RevCommit) = tabState.runOperation( + refreshType = RefreshType.ALL_DATA, ) { git -> - rebaseManager.rebaseInteractive(git, interactiveHandler, revCommit) - - val rebaseState = _rebaseState.value - - if (rebaseState is RebaseInteractiveState.Loaded) { - val messages = rebaseManager.rebaseLinesFullMessage(git, rebaseState.stepsList, revCommit) - _rebaseState.value = rebaseState.copy(messages = messages) + try { + rebaseManager.rebaseInteractive(git, interactiveHandler, revCommit) + completed = true + } catch (ex: RebaseCancelledException) { + println("Rebase cancelled") } } fun continueRebaseInteractive() = tabState.runOperation( refreshType = RefreshType.ONLY_LOG, - ) { git -> - val rebaseState = _rebaseState.value - if (rebaseState !is RebaseInteractiveState.Loaded) { - println("continueRebaseInteractive called when rebaseState is not Loaded") - return@runOperation // Should never happen, just in case - } - - val newSteps = rebaseState.stepsList - val rewordSteps = ArrayDeque(newSteps.filter { it.action == Action.REWORD }) - - rebaseManager.rebaseInteractive( - git = git, - interactiveHandler = object : InteractiveHandler { - override fun prepareSteps(steps: MutableList?) { - for (step in steps ?: emptyList()) { - val foundStep = newSteps.firstOrNull { it.commit.name() == step.commit.name() } - - if (foundStep != null) { - step.action = foundStep.action - } - } - } - - 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 - - return rebaseState.messages[step.commit.name()] - ?: throw InvalidMessageException("Message for commit $commit is unexpectedly null") - } - }, - commit = revCommit - ) + ) { + rebaseInteractiveMutex.unlock() } fun onCommitMessageChanged(commit: AbbreviatedObjectId, message: String) { @@ -115,6 +123,18 @@ class RebaseInteractiveViewModel @Inject constructor( _rebaseState.value = rebaseState.copy(stepsList = newStepsList) } } + + fun cancel() = tabState.runOperation( + refreshType = RefreshType.REPO_STATE + ) { git -> + if(!cancelled && !completed) { + rebaseManager.abortRebase(git) + + cancelled = true + + rebaseInteractiveMutex.unlock() + } + } } @@ -122,5 +142,4 @@ sealed interface RebaseInteractiveState { object Loading : RebaseInteractiveState data class Loaded(val stepsList: List, val messages: Map) : RebaseInteractiveState data class Failed(val error: String) : RebaseInteractiveState - object Finished : RebaseInteractiveState } \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/TabViewModel.kt b/src/main/kotlin/app/viewmodels/TabViewModel.kt index 24ad713..52bd226 100644 --- a/src/main/kotlin/app/viewmodels/TabViewModel.kt +++ b/src/main/kotlin/app/viewmodels/TabViewModel.kt @@ -19,6 +19,7 @@ import org.eclipse.jgit.lib.Repository import org.eclipse.jgit.lib.RepositoryState import java.io.File import javax.inject.Inject +import javax.inject.Provider private const val MIN_TIME_IN_MS_BETWEEN_REFRESHES = 1000L @@ -38,6 +39,7 @@ class TabViewModel @Inject constructor( val stashesViewModel: StashesViewModel, val commitChangesViewModel: CommitChangesViewModel, val cloneViewModel: CloneViewModel, + private val rebaseInteractiveViewModelProvider: Provider, private val repositoryManager: RepositoryManager, private val tabState: TabState, val appStateManager: AppStateManager, @@ -47,6 +49,9 @@ class TabViewModel @Inject constructor( val errorsManager: ErrorsManager = tabState.errorsManager val selectedItem: StateFlow = tabState.selectedItem + var rebaseInteractiveViewModel: RebaseInteractiveViewModel? = null + private set + private val credentialsStateManager = CredentialsStateManager private val _repositorySelectionStatus = MutableStateFlow(RepositorySelectionStatus.None) @@ -73,21 +78,42 @@ class TabViewModel @Inject constructor( val showError = MutableStateFlow(false) init { - tabState.managerScope.launch { - tabState.refreshData.collect { refreshType -> - when (refreshType) { - RefreshType.NONE -> println("Not refreshing...") - RefreshType.ALL_DATA -> refreshRepositoryInfo() - RefreshType.ONLY_LOG -> refreshLog() - RefreshType.STASHES -> refreshStashes() - RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges() - RefreshType.UNCOMMITED_CHANGES_AND_LOG -> checkUncommitedChanges(true) - RefreshType.REMOTES -> refreshRemotes() + tabState.managerScope.run { + launch { + tabState.refreshData.collect { refreshType -> + when (refreshType) { + RefreshType.NONE -> println("Not refreshing...") + RefreshType.ALL_DATA -> refreshRepositoryInfo() + RefreshType.REPO_STATE -> refreshRepositoryState() + RefreshType.ONLY_LOG -> refreshLog() + RefreshType.STASHES -> refreshStashes() + RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges() + RefreshType.UNCOMMITED_CHANGES_AND_LOG -> checkUncommitedChanges(true) + RefreshType.REMOTES -> refreshRemotes() + } + } + } + launch { + tabState.taskEvent.collect { taskEvent -> + when (taskEvent) { + is TaskEvent.RebaseInteractive -> onRebaseInteractive(taskEvent) + } } } } } + private fun refreshRepositoryState() = tabState.safeProcessing( + refreshType = RefreshType.NONE, + ) { git -> + loadRepositoryState(git) + } + + private suspend fun onRebaseInteractive(taskEvent: TaskEvent.RebaseInteractive) { + rebaseInteractiveViewModel = rebaseInteractiveViewModelProvider.get() + rebaseInteractiveViewModel?.startRebaseInteractive(taskEvent.revCommit) + } + private fun refreshRemotes() = tabState.runOperation( refreshType = RefreshType.NONE ) { git -> @@ -136,7 +162,18 @@ class TabViewModel @Inject constructor( } private suspend fun loadRepositoryState(git: Git) = withContext(Dispatchers.IO) { - _repositoryState.value = repositoryManager.getRepositoryState(git) + val newRepoState = repositoryManager.getRepositoryState(git) + println("Refreshing repository state $newRepoState") + _repositoryState.value = newRepoState + + onRepositoryStateChanged(newRepoState) + } + + private fun onRepositoryStateChanged(newRepoState: RepositoryState) { + if (newRepoState != RepositoryState.REBASING_INTERACTIVE && rebaseInteractiveViewModel != null) { + rebaseInteractiveViewModel?.cancel() + rebaseInteractiveViewModel = null + } } private suspend fun watchRepositoryChanges(git: Git) = tabState.managerScope.launch(Dispatchers.IO) { @@ -150,7 +187,7 @@ class TabViewModel @Inject constructor( if (!tabState.operationRunning) { // Only update if there isn't any process running println("Detected changes in the repository's directory") - if(latestUpdateChangedGitDir) { + if (latestUpdateChangedGitDir) { hasGitDirChanged = true } @@ -191,7 +228,7 @@ class TabViewModel @Inject constructor( } suspend fun updateApp(hasGitDirChanged: Boolean) { - if(hasGitDirChanged) { + if (hasGitDirChanged) { println("Changes detected in git directory, full refresh") refreshRepositoryInfo()