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) .setOperation(RebaseCommand.Operation.PROCESS_STEPS)
.call() .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 package com.jetpackduba.gitnuro.git.rebase
import com.jetpackduba.gitnuro.exceptions.UncommitedChangesDetectedException import com.jetpackduba.gitnuro.exceptions.UncommitedChangesDetectedException
import com.jetpackduba.gitnuro.logging.printDebug
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RebaseCommand import org.eclipse.jgit.api.RebaseCommand
import org.eclipse.jgit.api.RebaseResult import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.lib.RebaseTodoLine
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import java.io.File
import javax.inject.Inject import javax.inject.Inject
private const val GIT_REBASE_TODO = "git-rebase-todo"
private const val TAG = "StartRebaseInteractiveU"
class StartRebaseInteractiveUseCase @Inject constructor() { 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) { withContext(Dispatchers.IO) {
val rebaseResult = git.rebase() val rebaseResult = git.rebase()
.runInteractively(interactiveHandler) .runInteractively(interactiveHandler, stop)
.setOperation(RebaseCommand.Operation.BEGIN) .setOperation(RebaseCommand.Operation.BEGIN)
.setUpstream(commit) .setUpstream(commit)
.call() .call()
@ -26,5 +37,12 @@ class StartRebaseInteractiveUseCase @Inject constructor() {
else -> {} 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.AdjustableOutlinedTextField
import com.jetpackduba.gitnuro.ui.components.PrimaryButton import com.jetpackduba.gitnuro.ui.components.PrimaryButton
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
import com.jetpackduba.gitnuro.viewmodels.RebaseAction
import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveState import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveState
import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewModel import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewModel
import com.jetpackduba.gitnuro.viewmodels.RebaseLine
import org.eclipse.jgit.lib.RebaseTodoLine import org.eclipse.jgit.lib.RebaseTodoLine
import org.eclipse.jgit.lib.RebaseTodoLine.Action import org.eclipse.jgit.lib.RebaseTodoLine.Action
@ -99,7 +101,7 @@ fun RebaseStateLoaded(
) )
PrimaryButton( PrimaryButton(
modifier = Modifier.padding(end = 16.dp), modifier = Modifier.padding(end = 16.dp),
enabled = stepsList.any { it.action != Action.PICK }, enabled = stepsList.any { it.rebaseAction != RebaseAction.PICK },
onClick = { onClick = {
rebaseInteractiveViewModel.continueRebaseInteractive() rebaseInteractiveViewModel.continueRebaseInteractive()
}, },
@ -111,15 +113,15 @@ fun RebaseStateLoaded(
@Composable @Composable
fun RebaseCommit( fun RebaseCommit(
rebaseLine: RebaseTodoLine, rebaseLine: RebaseLine,
isFirst: Boolean, isFirst: Boolean,
message: String?, message: String?,
onActionChanged: (Action) -> Unit, onActionChanged: (RebaseAction) -> Unit,
onMessageChanged: (String) -> Unit, onMessageChanged: (String) -> Unit,
) { ) {
val action = rebaseLine.action val action = rebaseLine.rebaseAction
var newMessage by remember(rebaseLine.commit.name(), action) { 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)*/ mutableStateOf(message ?: rebaseLine.shortMessage) /* if reword, use the value from the map (if possible)*/
} else } else
mutableStateOf(rebaseLine.shortMessage) // If it's not reword, use the original shortMessage mutableStateOf(rebaseLine.shortMessage) // If it's not reword, use the original shortMessage
@ -132,7 +134,7 @@ fun RebaseCommit(
.fillMaxWidth() .fillMaxWidth()
) { ) {
ActionDropdown( ActionDropdown(
rebaseLine.action, action,
isFirst = isFirst, isFirst = isFirst,
onActionChanged = onActionChanged, onActionChanged = onActionChanged,
) )
@ -141,14 +143,14 @@ fun RebaseCommit(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.heightIn(min = 40.dp), .heightIn(min = 40.dp),
enabled = rebaseLine.action == Action.REWORD, enabled = action == RebaseAction.REWORD,
value = newMessage, value = newMessage,
onValueChange = { onValueChange = {
newMessage = it newMessage = it
onMessageChanged(it) onMessageChanged(it)
}, },
textStyle = MaterialTheme.typography.body2, textStyle = MaterialTheme.typography.body2,
backgroundColor = if (rebaseLine.action == Action.REWORD) { backgroundColor = if (action == RebaseAction.REWORD) {
MaterialTheme.colors.background MaterialTheme.colors.background
} else } else
MaterialTheme.colors.surface MaterialTheme.colors.surface
@ -160,9 +162,9 @@ fun RebaseCommit(
@Composable @Composable
fun ActionDropdown( fun ActionDropdown(
action: Action, action: RebaseAction,
isFirst: Boolean, isFirst: Boolean,
onActionChanged: (Action) -> Unit, onActionChanged: (RebaseAction) -> Unit,
) { ) {
var showDropDownMenu by remember { mutableStateOf(false) } var showDropDownMenu by remember { mutableStateOf(false) }
Box { Box {
@ -174,7 +176,7 @@ fun ActionDropdown(
.padding(end = 8.dp), .padding(end = 8.dp),
) { ) {
Text( Text(
action.toToken().replaceFirstChar { it.uppercase() }, action.displayName,
color = MaterialTheme.colors.onBackground, color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@ -206,7 +208,7 @@ fun ActionDropdown(
} }
) { ) {
Text( Text(
text = dropDownOption.toToken().replaceFirstChar { it.uppercase() }, text = dropDownOption.displayName,
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
) )
} }
@ -216,15 +218,15 @@ fun ActionDropdown(
} }
val firstItemActions = listOf( val firstItemActions = listOf(
Action.PICK, RebaseAction.PICK,
Action.REWORD, RebaseAction.REWORD,
) )
val actions = listOf( val actions = listOf(
Action.PICK, RebaseAction.PICK,
Action.REWORD, RebaseAction.REWORD,
Action.SQUASH, RebaseAction.SQUASH,
Action.FIXUP, 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 com.jetpackduba.gitnuro.git.rebase.StartRebaseInteractiveUseCase
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow 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.api.RebaseCommand.InteractiveHandler
import org.eclipse.jgit.lib.AbbreviatedObjectId import org.eclipse.jgit.lib.AbbreviatedObjectId
import org.eclipse.jgit.lib.RebaseTodoLine import org.eclipse.jgit.lib.RebaseTodoLine
@ -28,64 +26,69 @@ class RebaseInteractiveViewModel @Inject constructor(
private val abortRebaseUseCase: AbortRebaseUseCase, private val abortRebaseUseCase: AbortRebaseUseCase,
private val resumeRebaseInteractiveUseCase: ResumeRebaseInteractiveUseCase, private val resumeRebaseInteractiveUseCase: ResumeRebaseInteractiveUseCase,
) { ) {
private val rebaseInteractiveMutex = Mutex(true) private lateinit var commit: RevCommit
private val _rebaseState = MutableStateFlow<RebaseInteractiveState>(RebaseInteractiveState.Loading) private val _rebaseState = MutableStateFlow<RebaseInteractiveState>(RebaseInteractiveState.Loading)
val rebaseState: StateFlow<RebaseInteractiveState> = _rebaseState val rebaseState: StateFlow<RebaseInteractiveState> = _rebaseState
var rewordSteps = ArrayDeque<RebaseTodoLine>() var rewordSteps = ArrayDeque<RebaseLine>()
private var cancelled = false var onRebaseComplete: () -> Unit = {}
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")
}
private var interactiveHandlerContinue = object : InteractiveHandler {
override fun prepareSteps(steps: MutableList<RebaseTodoLine>) {
val rebaseState = _rebaseState.value val rebaseState = _rebaseState.value
if (rebaseState !is RebaseInteractiveState.Loaded) { if (rebaseState !is RebaseInteractiveState.Loaded) {
throw Exception("prepareSteps called when rebaseState is not Loaded") // Should never happen, just in case throw Exception("prepareSteps called when rebaseState is not Loaded") // Should never happen, just in case
} }
val newSteps = rebaseState.stepsList val newSteps = rebaseState.stepsList.toMutableList()
rewordSteps = ArrayDeque(newSteps.filter { it.action == Action.REWORD }) 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.clear()
steps.addAll(newSteps) steps.addAll(newRebaseTodoLines)
println("prepareSteps finished")
} }
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. // 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 val rebaseState = _rebaseState.value
if (rebaseState !is RebaseInteractiveState.Loaded) { if (rebaseState !is RebaseInteractiveState.Loaded) {
throw Exception("modifyCommitMessage called when rebaseState is not Loaded") // Should never happen, just in case 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") ?: 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, refreshType = RefreshType.ALL_DATA,
showError = true showError = true
) { git -> ) { git ->
this@RebaseInteractiveViewModel.commit = revCommit
val interactiveHandler = object : InteractiveHandler {
override fun prepareSteps(steps: MutableList<RebaseTodoLine>?) {}
override fun modifyCommitMessage(message: String?): String = ""
}
try { try {
startRebaseInteractiveUseCase(git, interactiveHandler, revCommit) val lines = startRebaseInteractiveUseCase(git, interactiveHandler, revCommit, true)
completed = 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) { } catch (ex: Exception) {
if (ex is RebaseCancelledException) { if (ex is RebaseCancelledException) {
println("Rebase cancelled") println("Rebase cancelled")
@ -96,10 +99,14 @@ class RebaseInteractiveViewModel @Inject constructor(
} }
} }
fun continueRebaseInteractive() = tabState.runOperation( fun continueRebaseInteractive() = tabState.safeProcessing(
refreshType = RefreshType.ONLY_LOG, refreshType = RefreshType.ALL_DATA,
) { ) { git ->
rebaseInteractiveMutex.unlock() try {
resumeRebaseInteractiveUseCase(git, interactiveHandlerContinue)
} finally {
onRebaseComplete()
}
} }
fun onCommitMessageChanged(commit: AbbreviatedObjectId, message: String) { fun onCommitMessageChanged(commit: AbbreviatedObjectId, message: String) {
@ -114,7 +121,7 @@ class RebaseInteractiveViewModel @Inject constructor(
_rebaseState.value = rebaseState.copy(messages = messagesMap) _rebaseState.value = rebaseState.copy(messages = messagesMap)
} }
fun onCommitActionChanged(commit: AbbreviatedObjectId, action: Action) { fun onCommitActionChanged(commit: AbbreviatedObjectId, rebaseAction: RebaseAction) {
val rebaseState = _rebaseState.value val rebaseState = _rebaseState.value
if (rebaseState !is RebaseInteractiveState.Loaded) if (rebaseState !is RebaseInteractiveState.Loaded)
@ -129,7 +136,12 @@ class RebaseInteractiveViewModel @Inject constructor(
if (stepIndex >= 0) { if (stepIndex >= 0) {
val step = newStepsList[stepIndex] val step = newStepsList[stepIndex]
val newTodoLine = RebaseTodoLine(action, step.commit, step.shortMessage) val newTodoLine = RebaseLine(
rebaseAction,
step.commit,
step.shortMessage
)
newStepsList[stepIndex] = newTodoLine newStepsList[stepIndex] = newTodoLine
_rebaseState.value = rebaseState.copy(stepsList = newStepsList) _rebaseState.value = rebaseState.copy(stepsList = newStepsList)
@ -139,36 +151,60 @@ class RebaseInteractiveViewModel @Inject constructor(
fun cancel() = tabState.runOperation( fun cancel() = tabState.runOperation(
refreshType = RefreshType.REPO_STATE refreshType = RefreshType.REPO_STATE
) { git -> ) { git ->
if (!cancelled && !completed) { abortRebaseUseCase(git)
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
}
}
} }
} }
sealed interface RebaseInteractiveState { sealed interface RebaseInteractiveState {
object Loading : 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 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) { private suspend fun onRebaseInteractive(taskEvent: TaskEvent.RebaseInteractive) {
rebaseInteractiveViewModel = rebaseInteractiveViewModelProvider.get() rebaseInteractiveViewModel = rebaseInteractiveViewModelProvider.get()
rebaseInteractiveViewModel?.startRebaseInteractive(taskEvent.revCommit) rebaseInteractiveViewModel?.startRebaseInteractive(taskEvent.revCommit)
rebaseInteractiveViewModel?.onRebaseComplete = {
rebaseInteractiveViewModel = null
}
} }
/** /**