Reimplemented rebase interactive as a part of the tab instead of a dialog

This commit is contained in:
Abdelilah El Aissaoui 2022-05-22 00:47:36 +02:00
parent 364fa53558
commit 51d79cff8f
11 changed files with 355 additions and 223 deletions

View File

@ -0,0 +1,3 @@
package app.exceptions
class RebaseCancelledException(msg: String) : GitnuroException(msg)

View File

@ -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<RebaseTodoLine>,
commit: RevCommit
): Map<String, String> = withContext(Dispatchers.IO) {
val revWalk = RevWalk(git.repository)
markCurrentBranchAsStart(revWalk, git)
val revCommitList = RevCommitList<RevCommit>()
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<ObjectId?> = 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
}
}

View File

@ -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>(SelectedItem.None)
val selectedItem: StateFlow<SelectedItem> = _selectedItem
private val _taskEvent = MutableSharedFlow<TaskEvent>()
val taskEvent: SharedFlow<TaskEvent> = _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,

View File

@ -0,0 +1,7 @@
package app.git
import org.eclipse.jgit.revwalk.RevCommit
sealed interface TaskEvent {
data class RebaseInteractive(val revCommit: RevCommit): TaskEvent
}

View File

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

View File

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

View File

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

View File

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

View File

@ -36,13 +36,9 @@ class LogViewModel @Inject constructor(
private val mergeManager: MergeManager,
private val remoteOperationsManager: RemoteOperationsManager,
private val tabState: TabState,
private val rebaseInteractiveViewModelProvider: Provider<RebaseInteractiveViewModel>
) {
private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading)
var rebaseInteractiveViewModel: RebaseInteractiveViewModel? = null
private set
val logStatus: StateFlow<LogStatus>
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 {

View File

@ -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>(RebaseInteractiveState.Loading)
val rebaseState: StateFlow<RebaseInteractiveState> = _rebaseState
var rewordSteps = ArrayDeque<RebaseTodoLine>()
private var cancelled = false
private var completed = false
private var interactiveHandler = object : InteractiveHandler {
override fun prepareSteps(steps: MutableList<RebaseTodoLine>?) {
_rebaseState.value = RebaseInteractiveState.Loaded(steps?.reversed() ?: emptyList(), emptyMap())
override fun prepareSteps(steps: MutableList<RebaseTodoLine>) = 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<RebaseTodoLine>(newSteps.filter { it.action == Action.REWORD })
rebaseManager.rebaseInteractive(
git = git,
interactiveHandler = object : InteractiveHandler {
override fun prepareSteps(steps: MutableList<RebaseTodoLine>?) {
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<RebaseTodoLine>, val messages: Map<String, String>) : RebaseInteractiveState
data class Failed(val error: String) : RebaseInteractiveState
object Finished : RebaseInteractiveState
}

View File

@ -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<RebaseInteractiveViewModel>,
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<SelectedItem> = tabState.selectedItem
var rebaseInteractiveViewModel: RebaseInteractiveViewModel? = null
private set
private val credentialsStateManager = CredentialsStateManager
private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(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()