Added first version of interactive rebase
This commit is contained in:
parent
08d0323e48
commit
14eb5f8c9c
@ -5,8 +5,10 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.api.RebaseCommand
|
||||
import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler
|
||||
import org.eclipse.jgit.api.RebaseResult
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
import javax.inject.Inject
|
||||
|
||||
class RebaseManager @Inject constructor() {
|
||||
@ -39,4 +41,13 @@ class RebaseManager @Inject constructor() {
|
||||
.setOperation(RebaseCommand.Operation.SKIP)
|
||||
.call()
|
||||
}
|
||||
|
||||
suspend fun rebaseInteractive(git: Git, interactiveHandler: InteractiveHandler, commit: RevCommit) {
|
||||
//TODO Check possible rebase errors by checking the result
|
||||
git.rebase()
|
||||
.runInteractively(interactiveHandler)
|
||||
.setOperation(RebaseCommand.Operation.BEGIN)
|
||||
.setUpstream(commit)
|
||||
.call()
|
||||
}
|
||||
}
|
@ -4,14 +4,11 @@ import app.ErrorsManager
|
||||
import app.di.TabScope
|
||||
import app.newErrorNow
|
||||
import app.ui.SelectedItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
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.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.eclipse.jgit.api.Git
|
||||
@ -127,7 +124,15 @@ class TabState @Inject constructor(
|
||||
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)
|
||||
|
@ -11,6 +11,7 @@ fun logContextMenu(
|
||||
onRevertCommit: () -> Unit,
|
||||
onCherryPickCommit: () -> Unit,
|
||||
onResetBranch: () -> Unit,
|
||||
onRebaseInteractive: () -> Unit,
|
||||
) = listOf(
|
||||
ContextMenuItem(
|
||||
label = "Checkout commit",
|
||||
@ -24,6 +25,10 @@ fun logContextMenu(
|
||||
label = "Create tag",
|
||||
onClick = onCreateNewTag
|
||||
),
|
||||
ContextMenuItem(
|
||||
label = "Rebase interactive",
|
||||
onClick = onRebaseInteractive
|
||||
),
|
||||
ContextMenuItem(
|
||||
label = "Revert commit",
|
||||
onClick = onRevertCommit
|
||||
|
176
src/main/kotlin/app/ui/dialogs/RebaseInteractiveDialog.kt
Normal file
176
src/main/kotlin/app/ui/dialogs/RebaseInteractiveDialog.kt
Normal file
@ -0,0 +1,176 @@
|
||||
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.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.theme.primaryTextColor
|
||||
import app.ui.components.PrimaryButton
|
||||
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(
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RebaseStateLoaded(
|
||||
rebaseInteractiveViewModel: RebaseInteractiveViewModel,
|
||||
rebaseState: RebaseInteractiveState.Loaded,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Text(
|
||||
text = "Rebase interactive",
|
||||
color = MaterialTheme.colors.primaryTextColor,
|
||||
modifier = Modifier.padding(all = 16.dp)
|
||||
)
|
||||
|
||||
LazyColumn(modifier = Modifier.weight(1f)) {
|
||||
items(rebaseState.stepsList) { rebaseTodoLine ->
|
||||
RebaseCommit(
|
||||
rebaseLine = rebaseTodoLine,
|
||||
onActionChanged = { newAction ->
|
||||
rebaseInteractiveViewModel.onCommitActionChanged(rebaseTodoLine.commit, newAction)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextButton(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
onClick = {
|
||||
onCancel()
|
||||
}
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
PrimaryButton(
|
||||
onClick = {
|
||||
rebaseInteractiveViewModel.continueRebaseInteractive()
|
||||
},
|
||||
text = "Complete rebase"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RebaseCommit(rebaseLine: RebaseTodoLine, onActionChanged: (Action) -> Unit) {
|
||||
Row (
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
ActionDropdown(
|
||||
rebaseLine.action,
|
||||
onActionChanged = onActionChanged,
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
value = rebaseLine.shortMessage,
|
||||
onValueChange = {},
|
||||
colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.background),
|
||||
textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ActionDropdown(
|
||||
action: Action,
|
||||
onActionChanged: (Action) -> Unit,
|
||||
) {
|
||||
var showDropDownMenu by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
PrimaryButton(
|
||||
onClick = { showDropDownMenu = true },
|
||||
modifier = Modifier
|
||||
.width(120.dp)
|
||||
.height(48.dp)
|
||||
.padding(end = 8.dp),
|
||||
text = action.toToken()
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = showDropDownMenu,
|
||||
onDismissRequest = { showDropDownMenu = false },
|
||||
) {
|
||||
for (dropDownOption in actions) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showDropDownMenu = false
|
||||
onActionChanged(dropDownOption)
|
||||
}
|
||||
) {
|
||||
Text(dropDownOption.toToken())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val actions = listOf(
|
||||
Action.PICK,
|
||||
Action.REWORD,
|
||||
// RebaseTodoLine.Action.EDIT,
|
||||
Action.SQUASH,
|
||||
Action.FIXUP,
|
||||
// RebaseTodoLine.Action.COMMENT,
|
||||
)
|
||||
|
||||
|
@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
@ -90,7 +89,7 @@ fun Log(
|
||||
val scope = rememberCoroutineScope()
|
||||
val logStatusState = logViewModel.logStatus.collectAsState()
|
||||
val logStatus = logStatusState.value
|
||||
val showLogDialog = remember { mutableStateOf<LogDialog>(LogDialog.None) }
|
||||
val showLogDialog by logViewModel.logDialog.collectAsState()
|
||||
|
||||
val selectedCommit = if (selectedItem is SelectedItem.CommitBasedItem) {
|
||||
selectedItem.revCommit
|
||||
@ -124,8 +123,8 @@ fun Log(
|
||||
LogDialogs(
|
||||
logViewModel,
|
||||
currentBranch = logStatus.currentBranch,
|
||||
onResetShowLogDialog = { showLogDialog.value = LogDialog.None },
|
||||
showLogDialog = showLogDialog.value,
|
||||
onResetShowLogDialog = { logViewModel.showDialog(LogDialog.None) },
|
||||
showLogDialog = showLogDialog,
|
||||
)
|
||||
|
||||
Column(
|
||||
@ -169,7 +168,7 @@ fun Log(
|
||||
logViewModel = logViewModel,
|
||||
graphWidth = graphWidth,
|
||||
onShowLogDialog = { dialog ->
|
||||
showLogDialog.value = dialog
|
||||
logViewModel.showDialog(dialog)
|
||||
})
|
||||
|
||||
DividerLog(
|
||||
@ -346,6 +345,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)) },
|
||||
onRevCommitSelected = { logViewModel.selectLogLine(graphNode) },
|
||||
)
|
||||
}
|
||||
@ -459,6 +459,13 @@ 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 -> {
|
||||
}
|
||||
}
|
||||
@ -635,6 +642,7 @@ fun CommitLine(
|
||||
onMergeBranch: (Ref) -> Unit,
|
||||
onRebaseBranch: (Ref) -> Unit,
|
||||
onRevCommitSelected: () -> Unit,
|
||||
onRebaseInteractive: () -> Unit,
|
||||
) {
|
||||
ContextMenuArea(
|
||||
items = {
|
||||
@ -644,6 +652,7 @@ fun CommitLine(
|
||||
onCreateNewTag = showCreateNewTag,
|
||||
onRevertCommit = { logViewModel.revertCommit(graphNode) },
|
||||
onCherryPickCommit = { logViewModel.cherrypickCommit(graphNode) },
|
||||
onRebaseInteractive = onRebaseInteractive,
|
||||
onResetBranch = { resetBranch() },
|
||||
)
|
||||
},
|
||||
|
@ -2,6 +2,7 @@ package app.ui.log
|
||||
|
||||
import app.git.graph.GraphNode
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
|
||||
sealed class LogDialog {
|
||||
object None : LogDialog()
|
||||
@ -10,4 +11,5 @@ 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()
|
||||
}
|
@ -5,6 +5,7 @@ import app.git.*
|
||||
import app.git.graph.GraphCommitList
|
||||
import app.git.graph.GraphNode
|
||||
import app.ui.SelectedItem
|
||||
import app.ui.log.LogDialog
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
@ -13,6 +14,7 @@ import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
/**
|
||||
* Represents when the search filter is not being used or the results list is empty
|
||||
@ -34,9 +36,13 @@ 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
|
||||
|
||||
@ -45,6 +51,9 @@ class LogViewModel @Inject constructor(
|
||||
private val _focusCommit = MutableSharedFlow<GraphNode>()
|
||||
val focusCommit: SharedFlow<GraphNode> = _focusCommit
|
||||
|
||||
private val _logDialog = MutableStateFlow<LogDialog>(LogDialog.None)
|
||||
val logDialog: StateFlow<LogDialog> = _logDialog
|
||||
|
||||
val lazyListState = MutableStateFlow(
|
||||
LazyListState(
|
||||
0,
|
||||
@ -302,6 +311,15 @@ class LogViewModel @Inject constructor(
|
||||
_focusCommit.emit(newCommitToSelect)
|
||||
}
|
||||
|
||||
fun showDialog(dialog: LogDialog) {
|
||||
rebaseInteractiveViewModel = if(dialog is LogDialog.RebaseInteractive) {
|
||||
rebaseInteractiveViewModelProvider.get()
|
||||
} else
|
||||
null
|
||||
|
||||
_logDialog.value = dialog
|
||||
}
|
||||
|
||||
fun closeSearch() {
|
||||
_logSearchFilterResults.value = LogSearch.NotSearching
|
||||
}
|
||||
|
99
src/main/kotlin/app/viewmodels/RebaseInteractiveViewModel.kt
Normal file
99
src/main/kotlin/app/viewmodels/RebaseInteractiveViewModel.kt
Normal file
@ -0,0 +1,99 @@
|
||||
package app.viewmodels
|
||||
|
||||
import app.git.RebaseManager
|
||||
import app.git.RefreshType
|
||||
import app.git.TabState
|
||||
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
|
||||
|
||||
class RebaseInteractiveViewModel @Inject constructor(
|
||||
private val tabState: TabState,
|
||||
private val rebaseManager: RebaseManager,
|
||||
) {
|
||||
lateinit var revCommit: RevCommit
|
||||
|
||||
private val _rebaseState = MutableStateFlow<RebaseInteractiveState>(RebaseInteractiveState.Loading)
|
||||
val rebaseState: StateFlow<RebaseInteractiveState> = _rebaseState
|
||||
|
||||
private var interactiveHandler = object : InteractiveHandler {
|
||||
override fun prepareSteps(steps: MutableList<RebaseTodoLine>?) {
|
||||
_rebaseState.value = RebaseInteractiveState.Loaded(steps?.reversed() ?: emptyList())
|
||||
}
|
||||
|
||||
override fun modifyCommitMessage(commit: String?): String {
|
||||
return commit.orEmpty() // we don't care about this since it's not called
|
||||
}
|
||||
}
|
||||
|
||||
fun startRebaseInteractive() = tabState.runOperation(
|
||||
refreshType = RefreshType.NONE,
|
||||
) { git ->
|
||||
rebaseManager.rebaseInteractive(git, interactiveHandler, revCommit)
|
||||
}
|
||||
|
||||
fun continueRebaseInteractive() = tabState.runOperation(
|
||||
refreshType = RefreshType.ONLY_LOG,
|
||||
) { git ->
|
||||
rebaseManager.rebaseInteractive(
|
||||
git = git,
|
||||
interactiveHandler = object : InteractiveHandler {
|
||||
override fun prepareSteps(steps: MutableList<RebaseTodoLine>?) {
|
||||
val rebaseState = _rebaseState.value
|
||||
|
||||
if(rebaseState !is RebaseInteractiveState.Loaded)
|
||||
return
|
||||
|
||||
val newSteps = rebaseState.stepsList
|
||||
|
||||
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 {
|
||||
return commit.orEmpty()
|
||||
}
|
||||
},
|
||||
commit = revCommit
|
||||
)
|
||||
}
|
||||
|
||||
fun onCommitActionChanged(commit: AbbreviatedObjectId, action: Action) {
|
||||
val rebaseState = _rebaseState.value
|
||||
|
||||
if(rebaseState !is RebaseInteractiveState.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 = RebaseTodoLine(action, step.commit, step.shortMessage)
|
||||
newStepsList[stepIndex] = newTodoLine
|
||||
|
||||
_rebaseState.value = RebaseInteractiveState.Loaded(newStepsList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sealed interface RebaseInteractiveState {
|
||||
object Loading : RebaseInteractiveState
|
||||
data class Loaded(val stepsList: List<RebaseTodoLine>) : RebaseInteractiveState
|
||||
data class Failed(val error: String) : RebaseInteractiveState
|
||||
object Finished : RebaseInteractiveState
|
||||
}
|
@ -255,6 +255,7 @@ class TabViewModel @Inject constructor(
|
||||
|
||||
private fun updateDiffEntry() {
|
||||
val diffSelected = diffSelected.value
|
||||
println("Update diff entry $diffSelected")
|
||||
|
||||
if (diffSelected != null) {
|
||||
diffViewModel.updateDiff(diffSelected)
|
||||
|
Loading…
Reference in New Issue
Block a user