Reimplemented rebase interactive as a part of the tab instead of a dialog
This commit is contained in:
parent
364fa53558
commit
51d79cff8f
@ -0,0 +1,3 @@
|
||||
package app.exceptions
|
||||
|
||||
class RebaseCancelledException(msg: String) : GitnuroException(msg)
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
7
src/main/kotlin/app/git/TaskEvent.kt
Normal file
7
src/main/kotlin/app/git/TaskEvent.kt
Normal file
@ -0,0 +1,7 @@
|
||||
package app.git
|
||||
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
|
||||
sealed interface TaskEvent {
|
||||
data class RebaseInteractive(val revCommit: RevCommit): TaskEvent
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 -> {
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user