Refactored rebase interactive to make it easier to implement additional features
This commit is contained in:
parent
15507afd4c
commit
1e012d759b
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user