Refactored rebase interactive to make it easier to implement additional features

This commit is contained in:
Abdelilah El Aissaoui 2023-06-29 21:42:33 +02:00
parent 15507afd4c
commit 1e012d759b
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
5 changed files with 153 additions and 88 deletions

View File

@ -16,8 +16,14 @@ class ResumeRebaseInteractiveUseCase @Inject constructor() {
.setOperation(RebaseCommand.Operation.PROCESS_STEPS)
.call()
if (rebaseResult.status == RebaseResult.Status.FAILED) {
throw UncommitedChangesDetectedException("Rebase interactive failed.")
when (rebaseResult.status) {
RebaseResult.Status.FAILED -> throw UncommitedChangesDetectedException("Rebase interactive failed.")
RebaseResult.Status.UNCOMMITTED_CHANGES, RebaseResult.Status.CONFLICTS -> throw UncommitedChangesDetectedException(
"You can't have uncommited changes before starting a rebase interactive"
)
else -> {}
}
}
}

View File

@ -1,19 +1,30 @@
package com.jetpackduba.gitnuro.git.rebase
import com.jetpackduba.gitnuro.exceptions.UncommitedChangesDetectedException
import com.jetpackduba.gitnuro.logging.printDebug
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.RebaseResult
import org.eclipse.jgit.lib.RebaseTodoLine
import org.eclipse.jgit.revwalk.RevCommit
import java.io.File
import javax.inject.Inject
private const val GIT_REBASE_TODO = "git-rebase-todo"
private const val TAG = "StartRebaseInteractiveU"
class StartRebaseInteractiveUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, interactiveHandler: RebaseCommand.InteractiveHandler, commit: RevCommit) =
suspend operator fun invoke(
git: Git,
interactiveHandler: RebaseCommand.InteractiveHandler,
commit: RevCommit,
stop: Boolean
): List<RebaseTodoLine> =
withContext(Dispatchers.IO) {
val rebaseResult = git.rebase()
.runInteractively(interactiveHandler)
.runInteractively(interactiveHandler, stop)
.setOperation(RebaseCommand.Operation.BEGIN)
.setUpstream(commit)
.call()
@ -26,5 +37,12 @@ class StartRebaseInteractiveUseCase @Inject constructor() {
else -> {}
}
val repository = git.repository
val lines = repository.readRebaseTodo("${RebaseCommand.REBASE_MERGE}/$GIT_REBASE_TODO", false)
printDebug(TAG, "There are ${lines.count()} lines")
return@withContext lines
}
}

View File

@ -15,8 +15,10 @@ import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField
import com.jetpackduba.gitnuro.ui.components.PrimaryButton
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
import com.jetpackduba.gitnuro.viewmodels.RebaseAction
import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveState
import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewModel
import com.jetpackduba.gitnuro.viewmodels.RebaseLine
import org.eclipse.jgit.lib.RebaseTodoLine
import org.eclipse.jgit.lib.RebaseTodoLine.Action
@ -99,7 +101,7 @@ fun RebaseStateLoaded(
)
PrimaryButton(
modifier = Modifier.padding(end = 16.dp),
enabled = stepsList.any { it.action != Action.PICK },
enabled = stepsList.any { it.rebaseAction != RebaseAction.PICK },
onClick = {
rebaseInteractiveViewModel.continueRebaseInteractive()
},
@ -111,15 +113,15 @@ fun RebaseStateLoaded(
@Composable
fun RebaseCommit(
rebaseLine: RebaseTodoLine,
rebaseLine: RebaseLine,
isFirst: Boolean,
message: String?,
onActionChanged: (Action) -> Unit,
onActionChanged: (RebaseAction) -> Unit,
onMessageChanged: (String) -> Unit,
) {
val action = rebaseLine.action
val action = rebaseLine.rebaseAction
var newMessage by remember(rebaseLine.commit.name(), action) {
if (action == Action.REWORD) {
if (action == RebaseAction.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
@ -132,7 +134,7 @@ fun RebaseCommit(
.fillMaxWidth()
) {
ActionDropdown(
rebaseLine.action,
action,
isFirst = isFirst,
onActionChanged = onActionChanged,
)
@ -141,14 +143,14 @@ fun RebaseCommit(
modifier = Modifier
.weight(1f)
.heightIn(min = 40.dp),
enabled = rebaseLine.action == Action.REWORD,
enabled = action == RebaseAction.REWORD,
value = newMessage,
onValueChange = {
newMessage = it
onMessageChanged(it)
},
textStyle = MaterialTheme.typography.body2,
backgroundColor = if (rebaseLine.action == Action.REWORD) {
backgroundColor = if (action == RebaseAction.REWORD) {
MaterialTheme.colors.background
} else
MaterialTheme.colors.surface
@ -160,9 +162,9 @@ fun RebaseCommit(
@Composable
fun ActionDropdown(
action: Action,
action: RebaseAction,
isFirst: Boolean,
onActionChanged: (Action) -> Unit,
onActionChanged: (RebaseAction) -> Unit,
) {
var showDropDownMenu by remember { mutableStateOf(false) }
Box {
@ -174,7 +176,7 @@ fun ActionDropdown(
.padding(end = 8.dp),
) {
Text(
action.toToken().replaceFirstChar { it.uppercase() },
action.displayName,
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.body1,
modifier = Modifier.weight(1f)
@ -206,7 +208,7 @@ fun ActionDropdown(
}
) {
Text(
text = dropDownOption.toToken().replaceFirstChar { it.uppercase() },
text = dropDownOption.displayName,
style = MaterialTheme.typography.body1,
)
}
@ -216,15 +218,15 @@ fun ActionDropdown(
}
val firstItemActions = listOf(
Action.PICK,
Action.REWORD,
RebaseAction.PICK,
RebaseAction.REWORD,
)
val actions = listOf(
Action.PICK,
Action.REWORD,
Action.SQUASH,
Action.FIXUP,
RebaseAction.PICK,
RebaseAction.REWORD,
RebaseAction.SQUASH,
RebaseAction.FIXUP,
RebaseAction.EDIT,
RebaseAction.DROP,
)

View File

@ -10,8 +10,6 @@ import com.jetpackduba.gitnuro.git.rebase.ResumeRebaseInteractiveUseCase
import com.jetpackduba.gitnuro.git.rebase.StartRebaseInteractiveUseCase
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
@ -28,64 +26,69 @@ class RebaseInteractiveViewModel @Inject constructor(
private val abortRebaseUseCase: AbortRebaseUseCase,
private val resumeRebaseInteractiveUseCase: ResumeRebaseInteractiveUseCase,
) {
private val rebaseInteractiveMutex = Mutex(true)
private lateinit var commit: RevCommit
private val _rebaseState = MutableStateFlow<RebaseInteractiveState>(RebaseInteractiveState.Loading)
val rebaseState: StateFlow<RebaseInteractiveState> = _rebaseState
var rewordSteps = ArrayDeque<RebaseTodoLine>()
var rewordSteps = ArrayDeque<RebaseLine>()
private var cancelled = false
private var completed = false
private var interactiveHandler = object : InteractiveHandler {
override fun prepareSteps(steps: MutableList<RebaseTodoLine>) = runBlocking {
println("prepareSteps started")
tabState.refreshData(RefreshType.REPO_STATE)
val messages = getRebaseLinesFullMessageUseCase(tabState.git, steps)
_rebaseState.value = RebaseInteractiveState.Loaded(steps, messages)
println("prepareSteps mutex lock")
rebaseInteractiveMutex.lock()
if (cancelled) {
throw RebaseCancelledException("Rebase cancelled due to user request")
}
var onRebaseComplete: () -> Unit = {}
private var interactiveHandlerContinue = object : InteractiveHandler {
override fun prepareSteps(steps: MutableList<RebaseTodoLine>) {
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 })
val newSteps = rebaseState.stepsList.toMutableList()
rewordSteps = ArrayDeque(newSteps.filter { it.rebaseAction == RebaseAction.REWORD })
val newRebaseTodoLines = newSteps
.filter { it.rebaseAction != RebaseAction.DROP } // Remove dropped lines
.map { it.toRebaseTodoLine() }
steps.clear()
steps.addAll(newSteps)
println("prepareSteps finished")
steps.addAll(newRebaseTodoLines)
}
override fun modifyCommitMessage(commit: String): String = runBlocking {
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@runBlocking commit
val step = rewordSteps.removeLastOrNull() ?: return 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()]
return rebaseState.messages[step.commit.name()]
?: throw InvalidMessageException("Message for commit $commit is unexpectedly null")
}
}
suspend fun startRebaseInteractive(revCommit: RevCommit) = tabState.runOperation(
suspend fun startRebaseInteractive(revCommit: RevCommit) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
showError = true
) { git ->
this@RebaseInteractiveViewModel.commit = revCommit
val interactiveHandler = object : InteractiveHandler {
override fun prepareSteps(steps: MutableList<RebaseTodoLine>?) {}
override fun modifyCommitMessage(message: String?): String = ""
}
try {
startRebaseInteractiveUseCase(git, interactiveHandler, revCommit)
completed = true
val lines = startRebaseInteractiveUseCase(git, interactiveHandler, revCommit, true)
val messages = getRebaseLinesFullMessageUseCase(tabState.git, lines)
val rebaseLines = lines.map {
RebaseLine(
it.action.toRebaseAction(),
it.commit,
it.shortMessage,
)
}
_rebaseState.value = RebaseInteractiveState.Loaded(rebaseLines, messages)
} catch (ex: Exception) {
if (ex is RebaseCancelledException) {
println("Rebase cancelled")
@ -96,10 +99,14 @@ class RebaseInteractiveViewModel @Inject constructor(
}
}
fun continueRebaseInteractive() = tabState.runOperation(
refreshType = RefreshType.ONLY_LOG,
) {
rebaseInteractiveMutex.unlock()
fun continueRebaseInteractive() = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
) { git ->
try {
resumeRebaseInteractiveUseCase(git, interactiveHandlerContinue)
} finally {
onRebaseComplete()
}
}
fun onCommitMessageChanged(commit: AbbreviatedObjectId, message: String) {
@ -114,7 +121,7 @@ class RebaseInteractiveViewModel @Inject constructor(
_rebaseState.value = rebaseState.copy(messages = messagesMap)
}
fun onCommitActionChanged(commit: AbbreviatedObjectId, action: Action) {
fun onCommitActionChanged(commit: AbbreviatedObjectId, rebaseAction: RebaseAction) {
val rebaseState = _rebaseState.value
if (rebaseState !is RebaseInteractiveState.Loaded)
@ -129,7 +136,12 @@ class RebaseInteractiveViewModel @Inject constructor(
if (stepIndex >= 0) {
val step = newStepsList[stepIndex]
val newTodoLine = RebaseTodoLine(action, step.commit, step.shortMessage)
val newTodoLine = RebaseLine(
rebaseAction,
step.commit,
step.shortMessage
)
newStepsList[stepIndex] = newTodoLine
_rebaseState.value = rebaseState.copy(stepsList = newStepsList)
@ -139,36 +151,60 @@ class RebaseInteractiveViewModel @Inject constructor(
fun cancel() = tabState.runOperation(
refreshType = RefreshType.REPO_STATE
) { git ->
if (!cancelled && !completed) {
abortRebaseUseCase(git)
cancelled = true
rebaseInteractiveMutex.unlock()
}
}
fun resumeRebase() = tabState.runOperation(
showError = true,
refreshType = RefreshType.NONE,
) { git ->
try {
resumeRebaseInteractiveUseCase(git, interactiveHandler)
completed = true
} catch (ex: Exception) {
if (ex is RebaseCancelledException) {
println("Rebase cancelled")
} else {
cancel()
throw ex
}
}
abortRebaseUseCase(git)
}
}
sealed interface RebaseInteractiveState {
object Loading : RebaseInteractiveState
data class Loaded(val stepsList: List<RebaseTodoLine>, val messages: Map<String, String>) : RebaseInteractiveState
data class Loaded(val stepsList: List<RebaseLine>, val messages: Map<String, String>) : RebaseInteractiveState
data class Failed(val error: String) : RebaseInteractiveState
}
data class RebaseLine(
val rebaseAction: RebaseAction,
val commit: AbbreviatedObjectId,
val shortMessage: String
) {
fun toRebaseTodoLine(): RebaseTodoLine {
return RebaseTodoLine(
rebaseAction.toAction(),
commit,
shortMessage
)
}
}
enum class RebaseAction(val displayName: String) {
PICK("Pick"),
REWORD("Reword"),
SQUASH("Squash"),
FIXUP("Fixup"),
EDIT("Edit"),
DROP("Drop"),
COMMENT("Comment");
fun toAction(): Action {
return when (this) {
PICK -> Action.PICK
REWORD -> Action.REWORD
SQUASH -> Action.SQUASH
FIXUP -> Action.FIXUP
EDIT -> Action.EDIT
COMMENT -> Action.COMMENT
DROP -> throw NotImplementedError("To action should not be called when the RebaseAction is DROP")
}
}
}
fun Action.toRebaseAction(): RebaseAction {
return when (this) {
Action.PICK -> RebaseAction.PICK
Action.REWORD -> RebaseAction.REWORD
Action.EDIT -> RebaseAction.EDIT
Action.SQUASH -> RebaseAction.SQUASH
Action.FIXUP -> RebaseAction.FIXUP
Action.COMMENT -> RebaseAction.COMMENT
}
}

View File

@ -165,6 +165,9 @@ class TabViewModel @Inject constructor(
private suspend fun onRebaseInteractive(taskEvent: TaskEvent.RebaseInteractive) {
rebaseInteractiveViewModel = rebaseInteractiveViewModelProvider.get()
rebaseInteractiveViewModel?.startRebaseInteractive(taskEvent.revCommit)
rebaseInteractiveViewModel?.onRebaseComplete = {
rebaseInteractiveViewModel = null
}
}
/**