Added first version of interactive rebase

This commit is contained in:
Abdelilah El Aissaoui 2022-04-09 23:10:08 +02:00
parent 08d0323e48
commit 14eb5f8c9c
9 changed files with 336 additions and 10 deletions

View File

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

View File

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

View File

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

View 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,
)

View File

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

View File

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

View File

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

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

View File

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