Implemented edit, drop & restore of Rebase interactive state even if started from external software

Fixes #143 and #65
This commit is contained in:
Abdelilah El Aissaoui 2023-07-01 21:50:53 +02:00
parent 1e012d759b
commit da9a5c1f17
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
19 changed files with 482 additions and 275 deletions

View File

@ -0,0 +1,58 @@
package com.jetpackduba.gitnuro
import com.jetpackduba.gitnuro.di.TabScope
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.rebase.GetRebaseInteractiveStateUseCase
import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState
import com.jetpackduba.gitnuro.git.repository.GetRepositoryStateUseCase
import com.jetpackduba.gitnuro.logging.printLog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.RepositoryState
import javax.inject.Inject
private const val TAG = "SharedRepositoryStateMa"
@TabScope
class SharedRepositoryStateManager @Inject constructor(
private val tabState: TabState,
private val getRebaseInteractiveStateUseCase: GetRebaseInteractiveStateUseCase,
private val getRepositoryStateUseCase: GetRepositoryStateUseCase,
tabScope: CoroutineScope,
) {
private val _repositoryState = MutableStateFlow(RepositoryState.SAFE)
val repositoryState = _repositoryState.asStateFlow()
private val _rebaseInteractiveState = MutableStateFlow<RebaseInteractiveState>(RebaseInteractiveState.None)
val rebaseInteractiveState = _rebaseInteractiveState.asStateFlow()
init {
tabScope.apply {
launch {
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REBASE_INTERACTIVE_STATE) {
updateRebaseInteractiveState(tabState.git)
}
}
launch {
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REPO_STATE) {
updateRepositoryState(tabState.git)
}
}
}
}
private suspend fun updateRepositoryState(git: Git) {
_repositoryState.value = getRepositoryStateUseCase(git)
}
private suspend fun updateRebaseInteractiveState(git: Git) {
val newRepositoryState = getRebaseInteractiveStateUseCase(git)
printLog(TAG, "Refreshing repository state $newRepositoryState")
_rebaseInteractiveState.value = newRepositoryState
}
}

View File

@ -3,6 +3,7 @@ package com.jetpackduba.gitnuro.git
import com.jetpackduba.gitnuro.managers.ErrorsManager import com.jetpackduba.gitnuro.managers.ErrorsManager
import com.jetpackduba.gitnuro.di.TabScope import com.jetpackduba.gitnuro.di.TabScope
import com.jetpackduba.gitnuro.extensions.delayedStateChange import com.jetpackduba.gitnuro.extensions.delayedStateChange
import com.jetpackduba.gitnuro.git.log.FindCommitUseCase
import com.jetpackduba.gitnuro.logging.printError import com.jetpackduba.gitnuro.logging.printError
import com.jetpackduba.gitnuro.managers.newErrorNow import com.jetpackduba.gitnuro.managers.newErrorNow
import com.jetpackduba.gitnuro.ui.SelectedItem import com.jetpackduba.gitnuro.ui.SelectedItem
@ -10,6 +11,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.RepositoryState
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject import javax.inject.Inject
@ -33,6 +35,7 @@ sealed interface ProcessingState {
class TabState @Inject constructor( class TabState @Inject constructor(
val errorsManager: ErrorsManager, val errorsManager: ErrorsManager,
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val findCommitUseCase: FindCommitUseCase,
) { ) {
private val _selectedItem = MutableStateFlow<SelectedItem>(SelectedItem.UncommitedChanges) private val _selectedItem = MutableStateFlow<SelectedItem>(SelectedItem.UncommitedChanges)
val selectedItem: StateFlow<SelectedItem> = _selectedItem val selectedItem: StateFlow<SelectedItem> = _selectedItem
@ -247,15 +250,16 @@ class TabState @Inject constructor(
if (objectId == null) { if (objectId == null) {
newSelectedItem(SelectedItem.None) newSelectedItem(SelectedItem.None)
} else { } else {
val commit = findCommit(git, objectId) val commit = findCommitUseCase(git, objectId)
val newSelectedItem = SelectedItem.Ref(commit)
newSelectedItem(newSelectedItem)
_taskEvent.emit(TaskEvent.ScrollToGraphItem(newSelectedItem))
}
}
private fun findCommit(git: Git, objectId: ObjectId): RevCommit { if(commit == null) {
return git.repository.parseCommit(objectId) newSelectedItem(SelectedItem.None)
} else {
val newSelectedItem = SelectedItem.Ref(commit)
newSelectedItem(newSelectedItem)
_taskEvent.emit(TaskEvent.ScrollToGraphItem(newSelectedItem))
}
}
} }
suspend fun newSelectedItem(selectedItem: SelectedItem, scrollToItem: Boolean = false) { suspend fun newSelectedItem(selectedItem: SelectedItem, scrollToItem: Boolean = false) {
@ -300,4 +304,5 @@ enum class RefreshType {
UNCOMMITED_CHANGES, UNCOMMITED_CHANGES,
UNCOMMITED_CHANGES_AND_LOG, UNCOMMITED_CHANGES_AND_LOG,
REMOTES, REMOTES,
REBASE_INTERACTIVE_STATE,
} }

View File

@ -4,6 +4,5 @@ import com.jetpackduba.gitnuro.ui.SelectedItem
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
sealed interface TaskEvent { sealed interface TaskEvent {
data class RebaseInteractive(val revCommit: RevCommit) : TaskEvent
data class ScrollToGraphItem(val selectedItem: SelectedItem) : TaskEvent data class ScrollToGraphItem(val selectedItem: SelectedItem) : TaskEvent
} }

View File

@ -0,0 +1,27 @@
package com.jetpackduba.gitnuro.git.log
import com.jetpackduba.gitnuro.logging.printError
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
private const val TAG = "FindCommitUseCase"
class FindCommitUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, commitId: String): RevCommit? = withContext(Dispatchers.IO) {
val objectId = ObjectId.fromString(commitId)
return@withContext invoke(git, objectId)
}
suspend operator fun invoke(git: Git, commitId: ObjectId): RevCommit? = withContext(Dispatchers.IO) {
return@withContext try {
git.repository.parseCommit(commitId)
} catch (ex: Exception) {
printError(TAG, "Commit $commitId not found", ex)
null
}
}
}

View File

@ -0,0 +1,14 @@
package com.jetpackduba.gitnuro.git.log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import javax.inject.Inject
class GetSpecificCommitMessageUseCase @Inject constructor(
private val findCommitUseCase: FindCommitUseCase,
) {
suspend operator fun invoke(git: Git, commitId: String): String = withContext(Dispatchers.IO) {
return@withContext findCommitUseCase(git, commitId)?.fullMessage.orEmpty()
}
}

View File

@ -0,0 +1,22 @@
package com.jetpackduba.gitnuro.git.rebase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RebaseCommand
import java.io.File
import javax.inject.Inject
class GetRebaseAmendCommitIdUseCase @Inject constructor() {
suspend operator fun invoke(git: Git): String? = withContext(Dispatchers.IO) {
val repository = git.repository
val amendFile = File(repository.directory, "${RebaseCommand.REBASE_MERGE}/${RebaseConstants.AMEND}")
if (!amendFile.exists()) {
return@withContext null
}
return@withContext amendFile.readText().removeSuffix("\n").removeSuffix("\r\n")
}
}

View File

@ -0,0 +1,29 @@
package com.jetpackduba.gitnuro.git.rebase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import java.io.File
import javax.inject.Inject
class GetRebaseInteractiveStateUseCase @Inject constructor(
private val getRebaseAmendCommitIdUseCase: GetRebaseAmendCommitIdUseCase,
) {
suspend operator fun invoke(git: Git): RebaseInteractiveState = withContext(Dispatchers.IO) {
val repository = git.repository
val rebaseMergeDir = File(repository.directory, RebaseConstants.REBASE_MERGE)
val doneFile = File(rebaseMergeDir, RebaseConstants.DONE)
val stoppedShaFile = File(rebaseMergeDir, RebaseConstants.STOPPED_SHA)
return@withContext when {
!rebaseMergeDir.exists() -> RebaseInteractiveState.None
doneFile.exists() || stoppedShaFile.exists() -> {
val commitId: String? = getRebaseAmendCommitIdUseCase(git)
RebaseInteractiveState.ProcessingCommits(commitId)
}
else -> RebaseInteractiveState.AwaitingInteraction
}
}
}

View File

@ -0,0 +1,24 @@
package com.jetpackduba.gitnuro.git.rebase
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.lib.RebaseTodoLine
import javax.inject.Inject
private const val TAG = "GetRebaseInteractiveTod"
class GetRebaseInteractiveTodoLinesUseCase @Inject constructor() {
suspend operator fun invoke(git: Git): List<RebaseTodoLine> = withContext(Dispatchers.IO) {
val repository = git.repository
val filePath = "${RebaseCommand.REBASE_MERGE}/${RebaseConstants.GIT_REBASE_TODO}"
val lines = repository.readRebaseTodo(filePath, false)
printDebug(TAG, "There are ${lines.count()} lines")
return@withContext lines
}
}

View File

@ -0,0 +1,10 @@
package com.jetpackduba.gitnuro.git.rebase
object RebaseConstants {
const val GIT_REBASE_TODO = "git-rebase-todo"
const val REBASE_MERGE = "rebase-merge" //$NON-NLS-1$
const val DONE = "done"
const val STOPPED_SHA = "stopped-sha"
const val AMEND = "amend"
}

View File

@ -0,0 +1,9 @@
package com.jetpackduba.gitnuro.git.rebase
sealed interface RebaseInteractiveState {
object None : RebaseInteractiveState
object AwaitingInteraction : RebaseInteractiveState
data class ProcessingCommits(val commitToAmendId: String?) : RebaseInteractiveState {
val isCurrentStepAmenable: Boolean = !commitToAmendId.isNullOrBlank()
}
}

View File

@ -1,7 +1,6 @@
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
@ -9,40 +8,33 @@ 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.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( suspend operator fun invoke(
git: Git, git: Git,
interactiveHandler: RebaseCommand.InteractiveHandler,
commit: RevCommit, commit: RevCommit,
stop: Boolean ) = withContext(Dispatchers.IO) {
): List<RebaseTodoLine> =
withContext(Dispatchers.IO) {
val rebaseResult = git.rebase()
.runInteractively(interactiveHandler, stop)
.setOperation(RebaseCommand.Operation.BEGIN)
.setUpstream(commit)
.call()
when (rebaseResult.status) { val interactiveHandler = object : RebaseCommand.InteractiveHandler {
RebaseResult.Status.FAILED -> throw UncommitedChangesDetectedException("Rebase interactive failed.") override fun prepareSteps(steps: MutableList<RebaseTodoLine>?) {}
RebaseResult.Status.UNCOMMITTED_CHANGES, RebaseResult.Status.CONFLICTS -> throw UncommitedChangesDetectedException( override fun modifyCommitMessage(message: String?): String = ""
"You can't have uncommited changes before starting a rebase interactive"
)
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
} }
val rebaseResult = git.rebase()
.runInteractively(interactiveHandler, true)
.setOperation(RebaseCommand.Operation.BEGIN)
.setUpstream(commit)
.call()
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

@ -15,16 +15,15 @@ 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.ui.components.gitnuroDynamicViewModel
import com.jetpackduba.gitnuro.viewmodels.RebaseAction import com.jetpackduba.gitnuro.viewmodels.RebaseAction
import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveState import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewState
import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewModel import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewModel
import com.jetpackduba.gitnuro.viewmodels.RebaseLine import com.jetpackduba.gitnuro.viewmodels.RebaseLine
import org.eclipse.jgit.lib.RebaseTodoLine
import org.eclipse.jgit.lib.RebaseTodoLine.Action
@Composable @Composable
fun RebaseInteractive( fun RebaseInteractive(
rebaseInteractiveViewModel: RebaseInteractiveViewModel, rebaseInteractiveViewModel: RebaseInteractiveViewModel = gitnuroDynamicViewModel(),
) { ) {
val rebaseState = rebaseInteractiveViewModel.rebaseState.collectAsState() val rebaseState = rebaseInteractiveViewModel.rebaseState.collectAsState()
val rebaseStateValue = rebaseState.value val rebaseStateValue = rebaseState.value
@ -35,8 +34,8 @@ fun RebaseInteractive(
.fillMaxSize(), .fillMaxSize(),
) { ) {
when (rebaseStateValue) { when (rebaseStateValue) {
is RebaseInteractiveState.Failed -> {} is RebaseInteractiveViewState.Failed -> {}
is RebaseInteractiveState.Loaded -> { is RebaseInteractiveViewState.Loaded -> {
RebaseStateLoaded( RebaseStateLoaded(
rebaseInteractiveViewModel, rebaseInteractiveViewModel,
rebaseStateValue, rebaseStateValue,
@ -46,7 +45,7 @@ fun RebaseInteractive(
) )
} }
RebaseInteractiveState.Loading -> { RebaseInteractiveViewState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} }
} }
@ -56,7 +55,7 @@ fun RebaseInteractive(
@Composable @Composable
fun RebaseStateLoaded( fun RebaseStateLoaded(
rebaseInteractiveViewModel: RebaseInteractiveViewModel, rebaseInteractiveViewModel: RebaseInteractiveViewModel,
rebaseState: RebaseInteractiveState.Loaded, rebaseState: RebaseInteractiveViewState.Loaded,
onCancel: () -> Unit, onCancel: () -> Unit,
) { ) {
val stepsList = rebaseState.stepsList val stepsList = rebaseState.stepsList
@ -220,6 +219,7 @@ fun ActionDropdown(
val firstItemActions = listOf( val firstItemActions = listOf(
RebaseAction.PICK, RebaseAction.PICK,
RebaseAction.REWORD, RebaseAction.REWORD,
RebaseAction.DROP,
) )
val actions = listOf( val actions = listOf(

View File

@ -18,11 +18,12 @@ import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppConstants import com.jetpackduba.gitnuro.AppConstants
import com.jetpackduba.gitnuro.LocalTabScope import com.jetpackduba.gitnuro.LocalTabScope
import com.jetpackduba.gitnuro.extensions.handMouseClickable import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.git.DiffEntryType import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState
import com.jetpackduba.gitnuro.keybindings.KeybindingOption import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.ui.components.PrimaryButton import com.jetpackduba.gitnuro.ui.components.PrimaryButton
@ -53,6 +54,7 @@ fun RepositoryOpenPage(
val blameState by tabViewModel.blameState.collectAsState() val blameState by tabViewModel.blameState.collectAsState()
val showHistory by tabViewModel.showHistory.collectAsState() val showHistory by tabViewModel.showHistory.collectAsState()
val showAuthorInfo by tabViewModel.showAuthorInfo.collectAsState() val showAuthorInfo by tabViewModel.showAuthorInfo.collectAsState()
val rebaseInteractiveState by tabViewModel.rebaseInteractiveState.collectAsState()
var showNewBranchDialog by remember { mutableStateOf(false) } var showNewBranchDialog by remember { mutableStateOf(false) }
var showStashWithMessageDialog by remember { mutableStateOf(false) } var showStashWithMessageDialog by remember { mutableStateOf(false) }
@ -129,14 +131,8 @@ fun RepositoryOpenPage(
} }
} }
) { ) {
val rebaseInteractiveViewModel = tabViewModel.rebaseInteractiveViewModel if (rebaseInteractiveState == RebaseInteractiveState.AwaitingInteraction) {
RebaseInteractive()
if (repositoryState == RepositoryState.REBASING_INTERACTIVE && rebaseInteractiveViewModel != null) {
RebaseInteractive(rebaseInteractiveViewModel)
} else if (repositoryState == RepositoryState.REBASING_INTERACTIVE) {
RebaseInteractiveStartedExternally(
onCancelRebaseInteractive = { tabViewModel.cancelRebaseInteractive() }
)
} else { } else {
val currentTabInformation = LocalTabScope.current val currentTabInformation = LocalTabScope.current
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
@ -366,45 +362,48 @@ fun MainContentView(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
) { ) {
val safeSelectedItem = selectedItem when (selectedItem) {
if (safeSelectedItem == SelectedItem.UncommitedChanges) { SelectedItem.UncommitedChanges -> {
UncommitedChanges( UncommitedChanges(
selectedEntryType = diffSelected, selectedEntryType = diffSelected,
repositoryState = repositoryState, repositoryState = repositoryState,
onStagedDiffEntrySelected = { diffEntry -> onStagedDiffEntrySelected = { diffEntry ->
tabViewModel.minimizeBlame() tabViewModel.minimizeBlame()
tabViewModel.newDiffSelected = if (diffEntry != null) {
if (repositoryState == RepositoryState.SAFE)
DiffEntryType.SafeStagedDiff(diffEntry)
else
DiffEntryType.UnsafeStagedDiff(diffEntry)
} else {
null
}
},
onUnstagedDiffEntrySelected = { diffEntry ->
tabViewModel.minimizeBlame()
tabViewModel.newDiffSelected = if (diffEntry != null) {
if (repositoryState == RepositoryState.SAFE) if (repositoryState == RepositoryState.SAFE)
DiffEntryType.SafeStagedDiff(diffEntry) tabViewModel.newDiffSelected = DiffEntryType.SafeUnstagedDiff(diffEntry)
else else
DiffEntryType.UnsafeStagedDiff(diffEntry) tabViewModel.newDiffSelected = DiffEntryType.UnsafeUnstagedDiff(diffEntry)
} else { },
null onBlameFile = { tabViewModel.blameFile(it) },
} onHistoryFile = { tabViewModel.fileHistory(it) }
}, )
onUnstagedDiffEntrySelected = { diffEntry -> }
tabViewModel.minimizeBlame() is SelectedItem.CommitBasedItem -> {
CommitChanges(
if (repositoryState == RepositoryState.SAFE) selectedItem = selectedItem,
tabViewModel.newDiffSelected = DiffEntryType.SafeUnstagedDiff(diffEntry) diffSelected = diffSelected,
else onDiffSelected = { diffEntry ->
tabViewModel.newDiffSelected = DiffEntryType.UnsafeUnstagedDiff(diffEntry) tabViewModel.minimizeBlame()
}, tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry)
onBlameFile = { tabViewModel.blameFile(it) }, },
onHistoryFile = { tabViewModel.fileHistory(it) } onBlame = { tabViewModel.blameFile(it) },
) onHistory = { tabViewModel.fileHistory(it) },
} else if (safeSelectedItem is SelectedItem.CommitBasedItem) { )
CommitChanges( }
selectedItem = safeSelectedItem, SelectedItem.None -> {}
diffSelected = diffSelected,
onDiffSelected = { diffEntry ->
tabViewModel.minimizeBlame()
tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry)
},
onBlame = { tabViewModel.blameFile(it) },
onHistory = { tabViewModel.fileHistory(it) },
)
} }
} }
} }

View File

@ -29,11 +29,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppIcons import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.extensions.* import com.jetpackduba.gitnuro.extensions.*
import com.jetpackduba.gitnuro.git.DiffEntryType import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState
import com.jetpackduba.gitnuro.git.workspace.StatusEntry import com.jetpackduba.gitnuro.git.workspace.StatusEntry
import com.jetpackduba.gitnuro.git.workspace.StatusType import com.jetpackduba.gitnuro.git.workspace.StatusType
import com.jetpackduba.gitnuro.keybindings.KeybindingOption import com.jetpackduba.gitnuro.keybindings.KeybindingOption
@ -66,14 +66,19 @@ fun UncommitedChanges(
val stagedListState by statusViewModel.stagedLazyListState.collectAsState() val stagedListState by statusViewModel.stagedLazyListState.collectAsState()
val unstagedListState by statusViewModel.unstagedLazyListState.collectAsState() val unstagedListState by statusViewModel.unstagedLazyListState.collectAsState()
val isAmend by statusViewModel.isAmend.collectAsState() val isAmend by statusViewModel.isAmend.collectAsState()
val isAmendRebaseInteractive by statusViewModel.isAmendRebaseInteractive.collectAsState()
val committerDataRequestState = statusViewModel.committerDataRequestState.collectAsState() val committerDataRequestState = statusViewModel.committerDataRequestState.collectAsState()
val committerDataRequestStateValue = committerDataRequestState.value val committerDataRequestStateValue = committerDataRequestState.value
val rebaseInteractiveState = statusViewModel.rebaseInteractiveState.collectAsState().value
val showSearchStaged by statusViewModel.showSearchStaged.collectAsState() val showSearchStaged by statusViewModel.showSearchStaged.collectAsState()
val searchFilterStaged by statusViewModel.searchFilterStaged.collectAsState() val searchFilterStaged by statusViewModel.searchFilterStaged.collectAsState()
val showSearchUnstaged by statusViewModel.showSearchUnstaged.collectAsState() val showSearchUnstaged by statusViewModel.showSearchUnstaged.collectAsState()
val searchFilterUnstaged by statusViewModel.searchFilterUnstaged.collectAsState() val searchFilterUnstaged by statusViewModel.searchFilterUnstaged.collectAsState()
val isAmenableRebaseInteractive =
repositoryState.isRebasing && rebaseInteractiveState is RebaseInteractiveState.ProcessingCommits && rebaseInteractiveState.isCurrentStepAmenable
val staged: List<StatusEntry> val staged: List<StatusEntry>
val unstaged: List<StatusEntry> val unstaged: List<StatusEntry>
val isLoading: Boolean val isLoading: Boolean
@ -247,9 +252,9 @@ fun UncommitedChanges(
statusViewModel.updateCommitMessage(it) statusViewModel.updateCommitMessage(it)
}, },
enabled = !repositoryState.isRebasing, enabled = !repositoryState.isRebasing || isAmenableRebaseInteractive,
label = { label = {
val text = if (repositoryState.isRebasing) { val text = if (repositoryState.isRebasing && !isAmenableRebaseInteractive) {
"Commit message (read-only)" "Commit message (read-only)"
} else { } else {
"Write your commit message here" "Write your commit message here"
@ -275,15 +280,20 @@ fun UncommitedChanges(
onMerge = { doCommit() } onMerge = { doCommit() }
) )
repositoryState.isRebasing -> RebasingButtons( repositoryState.isRebasing && rebaseInteractiveState is RebaseInteractiveState.ProcessingCommits -> RebasingButtons(
canContinue = staged.isNotEmpty() || unstaged.isNotEmpty(), canContinue = staged.isNotEmpty() || unstaged.isNotEmpty() || (isAmenableRebaseInteractive && isAmendRebaseInteractive && commitMessage.isNotEmpty()),
haveConflictsBeenSolved = unstaged.isEmpty(), haveConflictsBeenSolved = unstaged.isEmpty(),
onAbort = { onAbort = {
statusViewModel.abortRebase() statusViewModel.abortRebase()
statusViewModel.updateCommitMessage("") statusViewModel.updateCommitMessage("")
}, },
onContinue = { statusViewModel.continueRebase() }, onContinue = { statusViewModel.continueRebase(commitMessage) },
onSkip = { statusViewModel.skipRebase() }, onSkip = { statusViewModel.skipRebase() },
isAmendable = rebaseInteractiveState.isCurrentStepAmenable,
isAmend = isAmendRebaseInteractive,
onAmendChecked = { isAmend ->
statusViewModel.amendRebaseInteractive(isAmend)
}
) )
repositoryState.isCherryPicking -> CherryPickingButtons( repositoryState.isCherryPicking -> CherryPickingButtons(
@ -338,31 +348,11 @@ fun UncommitedChangesButtons(
"Commit" "Commit"
Column { Column {
Row( CheckboxText(
verticalAlignment = Alignment.CenterVertically, value = isAmend,
modifier = Modifier.handMouseClickable( onCheckedChange = { onAmendChecked(!isAmend) },
interactionSource = remember { MutableInteractionSource() }, text = "Amend previous commit"
indication = null, )
) {
onAmendChecked(!isAmend)
}
) {
Checkbox(
checked = isAmend,
onCheckedChange = {
onAmendChecked(!isAmend)
},
modifier = Modifier
.padding(all = 8.dp)
.size(12.dp)
)
Text(
"Amend previous commit",
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onBackground,
)
}
Row( Row(
modifier = Modifier modifier = Modifier
.padding(top = 2.dp) .padding(top = 2.dp)
@ -439,40 +429,52 @@ fun CherryPickingButtons(
@Composable @Composable
fun RebasingButtons( fun RebasingButtons(
canContinue: Boolean, canContinue: Boolean,
isAmendable: Boolean,
isAmend: Boolean,
onAmendChecked: (Boolean) -> Unit,
haveConflictsBeenSolved: Boolean, haveConflictsBeenSolved: Boolean,
onAbort: () -> Unit, onAbort: () -> Unit,
onContinue: () -> Unit, onContinue: () -> Unit,
onSkip: () -> Unit, onSkip: () -> Unit,
) { ) {
Row( Column {
modifier = Modifier.fillMaxWidth() if (isAmendable) {
) { CheckboxText(
AbortButton( value = isAmend,
modifier = Modifier onCheckedChange = { onAmendChecked(!isAmend) },
.weight(1f) text = "Amend previous commit"
.padding(end = 4.dp),
onClick = onAbort
)
if (canContinue) {
ConfirmationButton(
text = "Continue",
modifier = Modifier
.weight(1f)
.padding(start = 4.dp),
enabled = haveConflictsBeenSolved,
onClick = onContinue,
)
} else {
ConfirmationButton(
text = "Skip",
modifier = Modifier
.weight(1f)
.padding(end = 4.dp),
onClick = onSkip,
) )
} }
Row(
modifier = Modifier.fillMaxWidth()
) {
AbortButton(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp),
onClick = onAbort
)
if (canContinue) {
ConfirmationButton(
text = "Continue",
modifier = Modifier
.weight(1f)
.padding(start = 4.dp),
enabled = haveConflictsBeenSolved,
onClick = onContinue,
)
} else {
ConfirmationButton(
text = "Skip",
modifier = Modifier
.weight(1f)
.padding(start = 4.dp),
onClick = onSkip,
)
}
}
} }
} }
@ -759,18 +761,6 @@ private fun FileEntry(
} }
} }
@Stable
val BottomReversed = object : Arrangement.Vertical {
override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray
) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = true)
override fun toString() = "Arrangement#BottomReversed"
}
internal fun placeRightOrBottom( internal fun placeRightOrBottom(
totalSize: Int, totalSize: Int,
size: IntArray, size: IntArray,

View File

@ -0,0 +1,45 @@
package com.jetpackduba.gitnuro.ui.components
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Checkbox
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.extensions.handMouseClickable
@Composable
fun CheckboxText(
value: Boolean,
onCheckedChange: () -> Unit,
text: String,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.handMouseClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onCheckedChange,
)
) {
Checkbox(
checked = value,
onCheckedChange = { onCheckedChange() },
modifier = Modifier
.padding(all = 8.dp)
.size(12.dp)
)
Text(
text,
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onBackground,
)
}
}

View File

@ -13,6 +13,7 @@ import com.jetpackduba.gitnuro.git.graph.GraphCommitList
import com.jetpackduba.gitnuro.git.graph.GraphNode import com.jetpackduba.gitnuro.git.graph.GraphNode
import com.jetpackduba.gitnuro.git.log.* import com.jetpackduba.gitnuro.git.log.*
import com.jetpackduba.gitnuro.git.rebase.RebaseBranchUseCase import com.jetpackduba.gitnuro.git.rebase.RebaseBranchUseCase
import com.jetpackduba.gitnuro.git.rebase.StartRebaseInteractiveUseCase
import com.jetpackduba.gitnuro.git.remote_operations.DeleteRemoteBranchUseCase import com.jetpackduba.gitnuro.git.remote_operations.DeleteRemoteBranchUseCase
import com.jetpackduba.gitnuro.git.remote_operations.PullFromSpecificBranchUseCase import com.jetpackduba.gitnuro.git.remote_operations.PullFromSpecificBranchUseCase
import com.jetpackduba.gitnuro.git.remote_operations.PushToSpecificBranchUseCase import com.jetpackduba.gitnuro.git.remote_operations.PushToSpecificBranchUseCase
@ -65,6 +66,7 @@ class LogViewModel @Inject constructor(
private val createTagOnCommitUseCase: CreateTagOnCommitUseCase, private val createTagOnCommitUseCase: CreateTagOnCommitUseCase,
private val deleteTagUseCase: DeleteTagUseCase, private val deleteTagUseCase: DeleteTagUseCase,
private val rebaseBranchUseCase: RebaseBranchUseCase, private val rebaseBranchUseCase: RebaseBranchUseCase,
private val startRebaseInteractiveUseCase: StartRebaseInteractiveUseCase,
private val tabState: TabState, private val tabState: TabState,
private val appSettings: AppSettings, private val appSettings: AppSettings,
private val tabScope: CoroutineScope, private val tabScope: CoroutineScope,
@ -97,7 +99,6 @@ class LogViewModel @Inject constructor(
val verticalListState = MutableStateFlow(LazyListState(0, 0)) val verticalListState = MutableStateFlow(LazyListState(0, 0))
val horizontalListState = MutableStateFlow(ScrollState(0)) val horizontalListState = MutableStateFlow(ScrollState(0))
private val _logSearchFilterResults = MutableStateFlow<LogSearch>(LogSearch.NotSearching) private val _logSearchFilterResults = MutableStateFlow<LogSearch>(LogSearch.NotSearching)
val logSearchFilterResults: StateFlow<LogSearch> = _logSearchFilterResults val logSearchFilterResults: StateFlow<LogSearch> = _logSearchFilterResults
@ -428,10 +429,10 @@ class LogViewModel @Inject constructor(
_logSearchFilterResults.value = LogSearch.NotSearching _logSearchFilterResults.value = LogSearch.NotSearching
} }
fun rebaseInteractive(revCommit: RevCommit) = tabState.runOperation( fun rebaseInteractive(revCommit: RevCommit) = tabState.safeProcessing(
refreshType = RefreshType.NONE refreshType = RefreshType.REBASE_INTERACTIVE_STATE,
) { ) { git ->
tabState.emitNewTaskEvent(TaskEvent.RebaseInteractive(revCommit)) startRebaseInteractiveUseCase(git, revCommit)
} }
fun deleteRemoteBranch(branch: Ref) = tabState.safeProcessing( fun deleteRemoteBranch(branch: Ref) = tabState.safeProcessing(

View File

@ -4,10 +4,7 @@ import com.jetpackduba.gitnuro.exceptions.InvalidMessageException
import com.jetpackduba.gitnuro.exceptions.RebaseCancelledException import com.jetpackduba.gitnuro.exceptions.RebaseCancelledException
import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.rebase.AbortRebaseUseCase import com.jetpackduba.gitnuro.git.rebase.*
import com.jetpackduba.gitnuro.git.rebase.GetRebaseLinesFullMessageUseCase
import com.jetpackduba.gitnuro.git.rebase.ResumeRebaseInteractiveUseCase
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 org.eclipse.jgit.api.RebaseCommand.InteractiveHandler import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler
@ -22,21 +19,23 @@ private const val TAG = "RebaseInteractiveViewMo"
class RebaseInteractiveViewModel @Inject constructor( class RebaseInteractiveViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val getRebaseLinesFullMessageUseCase: GetRebaseLinesFullMessageUseCase, private val getRebaseLinesFullMessageUseCase: GetRebaseLinesFullMessageUseCase,
private val startRebaseInteractiveUseCase: StartRebaseInteractiveUseCase, private val getRebaseInteractiveTodoLinesUseCase: GetRebaseInteractiveTodoLinesUseCase,
private val abortRebaseUseCase: AbortRebaseUseCase, private val abortRebaseUseCase: AbortRebaseUseCase,
private val resumeRebaseInteractiveUseCase: ResumeRebaseInteractiveUseCase, private val resumeRebaseInteractiveUseCase: ResumeRebaseInteractiveUseCase,
) { ) {
private lateinit var commit: RevCommit private lateinit var commit: RevCommit
private val _rebaseState = MutableStateFlow<RebaseInteractiveState>(RebaseInteractiveState.Loading) private val _rebaseState = MutableStateFlow<RebaseInteractiveViewState>(RebaseInteractiveViewState.Loading)
val rebaseState: StateFlow<RebaseInteractiveState> = _rebaseState val rebaseState: StateFlow<RebaseInteractiveViewState> = _rebaseState
var rewordSteps = ArrayDeque<RebaseLine>() var rewordSteps = ArrayDeque<RebaseLine>()
var onRebaseComplete: () -> Unit = {} init {
loadRebaseInteractiveData()
}
private var interactiveHandlerContinue = object : InteractiveHandler { private var interactiveHandlerContinue = object : InteractiveHandler {
override fun prepareSteps(steps: MutableList<RebaseTodoLine>) { override fun prepareSteps(steps: MutableList<RebaseTodoLine>) {
val rebaseState = _rebaseState.value val rebaseState = _rebaseState.value
if (rebaseState !is RebaseInteractiveState.Loaded) { if (rebaseState !is RebaseInteractiveViewState.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
} }
@ -56,7 +55,7 @@ class RebaseInteractiveViewModel @Inject constructor(
val step = rewordSteps.removeLastOrNull() ?: return commit val step = rewordSteps.removeLastOrNull() ?: return commit
val rebaseState = _rebaseState.value val rebaseState = _rebaseState.value
if (rebaseState !is RebaseInteractiveState.Loaded) { if (rebaseState !is RebaseInteractiveViewState.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
} }
@ -65,19 +64,11 @@ class RebaseInteractiveViewModel @Inject constructor(
} }
} }
suspend fun startRebaseInteractive(revCommit: RevCommit) = tabState.safeProcessing( private fun loadRebaseInteractiveData() = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.NONE,
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 {
val lines = startRebaseInteractiveUseCase(git, interactiveHandler, revCommit, true) val lines = getRebaseInteractiveTodoLinesUseCase(git)
val messages = getRebaseLinesFullMessageUseCase(tabState.git, lines) val messages = getRebaseLinesFullMessageUseCase(tabState.git, lines)
val rebaseLines = lines.map { val rebaseLines = lines.map {
RebaseLine( RebaseLine(
@ -87,7 +78,7 @@ class RebaseInteractiveViewModel @Inject constructor(
) )
} }
_rebaseState.value = RebaseInteractiveState.Loaded(rebaseLines, messages) _rebaseState.value = RebaseInteractiveViewState.Loaded(rebaseLines, messages)
} catch (ex: Exception) { } catch (ex: Exception) {
if (ex is RebaseCancelledException) { if (ex is RebaseCancelledException) {
@ -102,17 +93,13 @@ class RebaseInteractiveViewModel @Inject constructor(
fun continueRebaseInteractive() = tabState.safeProcessing( fun continueRebaseInteractive() = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
try { resumeRebaseInteractiveUseCase(git, interactiveHandlerContinue)
resumeRebaseInteractiveUseCase(git, interactiveHandlerContinue)
} finally {
onRebaseComplete()
}
} }
fun onCommitMessageChanged(commit: AbbreviatedObjectId, message: String) { fun onCommitMessageChanged(commit: AbbreviatedObjectId, message: String) {
val rebaseState = _rebaseState.value val rebaseState = _rebaseState.value
if (rebaseState !is RebaseInteractiveState.Loaded) if (rebaseState !is RebaseInteractiveViewState.Loaded)
return return
val messagesMap = rebaseState.messages.toMutableMap() val messagesMap = rebaseState.messages.toMutableMap()
@ -124,7 +111,7 @@ class RebaseInteractiveViewModel @Inject constructor(
fun onCommitActionChanged(commit: AbbreviatedObjectId, rebaseAction: RebaseAction) { fun onCommitActionChanged(commit: AbbreviatedObjectId, rebaseAction: RebaseAction) {
val rebaseState = _rebaseState.value val rebaseState = _rebaseState.value
if (rebaseState !is RebaseInteractiveState.Loaded) if (rebaseState !is RebaseInteractiveViewState.Loaded)
return return
val newStepsList = val newStepsList =
@ -149,17 +136,17 @@ class RebaseInteractiveViewModel @Inject constructor(
} }
fun cancel() = tabState.runOperation( fun cancel() = tabState.runOperation(
refreshType = RefreshType.REPO_STATE refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
abortRebaseUseCase(git) abortRebaseUseCase(git)
} }
} }
sealed interface RebaseInteractiveState { sealed interface RebaseInteractiveViewState {
object Loading : RebaseInteractiveState object Loading : RebaseInteractiveViewState
data class Loaded(val stepsList: List<RebaseLine>, val messages: Map<String, String>) : RebaseInteractiveState data class Loaded(val stepsList: List<RebaseLine>, val messages: Map<String, String>) : RebaseInteractiveViewState
data class Failed(val error: String) : RebaseInteractiveState data class Failed(val error: String) : RebaseInteractiveViewState
} }
data class RebaseLine( data class RebaseLine(

View File

@ -2,6 +2,7 @@ package com.jetpackduba.gitnuro.viewmodels
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import com.jetpackduba.gitnuro.SharedRepositoryStateManager
import com.jetpackduba.gitnuro.extensions.delayedStateChange import com.jetpackduba.gitnuro.extensions.delayedStateChange
import com.jetpackduba.gitnuro.extensions.isMerging import com.jetpackduba.gitnuro.extensions.isMerging
import com.jetpackduba.gitnuro.extensions.isReverting import com.jetpackduba.gitnuro.extensions.isReverting
@ -12,9 +13,9 @@ import com.jetpackduba.gitnuro.git.author.LoadAuthorUseCase
import com.jetpackduba.gitnuro.git.author.SaveAuthorUseCase import com.jetpackduba.gitnuro.git.author.SaveAuthorUseCase
import com.jetpackduba.gitnuro.git.log.CheckHasPreviousCommitsUseCase import com.jetpackduba.gitnuro.git.log.CheckHasPreviousCommitsUseCase
import com.jetpackduba.gitnuro.git.log.GetLastCommitMessageUseCase import com.jetpackduba.gitnuro.git.log.GetLastCommitMessageUseCase
import com.jetpackduba.gitnuro.git.rebase.AbortRebaseUseCase import com.jetpackduba.gitnuro.git.log.GetSpecificCommitMessageUseCase
import com.jetpackduba.gitnuro.git.rebase.ContinueRebaseUseCase import com.jetpackduba.gitnuro.git.rebase.*
import com.jetpackduba.gitnuro.git.rebase.SkipRebaseUseCase import com.jetpackduba.gitnuro.git.repository.GetRepositoryStateUseCase
import com.jetpackduba.gitnuro.git.repository.ResetRepositoryStateUseCase import com.jetpackduba.gitnuro.git.repository.ResetRepositoryStateUseCase
import com.jetpackduba.gitnuro.git.workspace.* import com.jetpackduba.gitnuro.git.workspace.*
import com.jetpackduba.gitnuro.models.AuthorInfo import com.jetpackduba.gitnuro.models.AuthorInfo
@ -52,6 +53,10 @@ class StatusViewModel @Inject constructor(
private val doCommitUseCase: DoCommitUseCase, private val doCommitUseCase: DoCommitUseCase,
private val loadAuthorUseCase: LoadAuthorUseCase, private val loadAuthorUseCase: LoadAuthorUseCase,
private val saveAuthorUseCase: SaveAuthorUseCase, private val saveAuthorUseCase: SaveAuthorUseCase,
private val getRepositoryStateUseCase: GetRepositoryStateUseCase,
private val getRebaseAmendCommitIdUseCase: GetRebaseAmendCommitIdUseCase,
private val sharedRepositoryStateManager: SharedRepositoryStateManager,
private val getSpecificCommitMessageUseCase: GetSpecificCommitMessageUseCase,
private val tabScope: CoroutineScope, private val tabScope: CoroutineScope,
private val appSettings: AppSettings, private val appSettings: AppSettings,
) { ) {
@ -68,6 +73,7 @@ class StatusViewModel @Inject constructor(
val searchFilterStaged: StateFlow<TextFieldValue> = _searchFilterStaged val searchFilterStaged: StateFlow<TextFieldValue> = _searchFilterStaged
val swapUncommitedChanges = appSettings.swapUncommitedChangesFlow val swapUncommitedChanges = appSettings.swapUncommitedChangesFlow
val rebaseInteractiveState = sharedRepositoryStateManager.rebaseInteractiveState
private val _stageState = MutableStateFlow<StageState>(StageState.Loading) private val _stageState = MutableStateFlow<StageState>(StageState.Loading)
@ -123,6 +129,9 @@ class StatusViewModel @Inject constructor(
private val _isAmend = MutableStateFlow(false) private val _isAmend = MutableStateFlow(false)
val isAmend: StateFlow<Boolean> = _isAmend val isAmend: StateFlow<Boolean> = _isAmend
private val _isAmendRebaseInteractive = MutableStateFlow(false)
val isAmendRebaseInteractive: StateFlow<Boolean> = _isAmendRebaseInteractive
init { init {
tabScope.launch { tabScope.launch {
tabState.refreshFlowFiltered( tabState.refreshFlowFiltered(
@ -262,6 +271,14 @@ class StatusViewModel @Inject constructor(
} }
} }
fun amendRebaseInteractive(isAmend: Boolean) {
_isAmendRebaseInteractive.value = isAmend
if (isAmend && savedCommitMessage.message.isEmpty()) {
takeMessageFromAmendCommit()
}
}
fun commit(message: String) = tabState.safeProcessing( fun commit(message: String) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
@ -272,9 +289,18 @@ class StatusViewModel @Inject constructor(
} else } else
message message
val personIdent = getPersonIdent(git)
doCommitUseCase(git, commitMessage, amend, personIdent)
updateCommitMessage("")
_isAmend.value = false
}
private suspend fun getPersonIdent(git: Git): PersonIdent? {
val author = loadAuthorUseCase(git) val author = loadAuthorUseCase(git)
val personIdent = if ( return if (
author.name.isNullOrEmpty() && author.globalName.isNullOrEmpty() || author.name.isNullOrEmpty() && author.globalName.isNullOrEmpty() ||
author.email.isNullOrEmpty() && author.globalEmail.isNullOrEmpty() author.email.isNullOrEmpty() && author.globalEmail.isNullOrEmpty()
) { ) {
@ -299,10 +325,6 @@ class StatusViewModel @Inject constructor(
} }
} else } else
null null
doCommitUseCase(git, commitMessage, amend, personIdent)
updateCommitMessage("")
_isAmend.value = false
} }
suspend fun refresh(git: Git) = withContext(Dispatchers.IO) { suspend fun refresh(git: Git) = withContext(Dispatchers.IO) {
@ -326,9 +348,25 @@ class StatusViewModel @Inject constructor(
return (hasNowUncommitedChanges != hadUncommitedChanges) return (hasNowUncommitedChanges != hadUncommitedChanges)
} }
fun continueRebase() = tabState.safeProcessing( fun continueRebase(message: String) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
val repositoryState = sharedRepositoryStateManager.repositoryState.value
val rebaseInteractiveState = sharedRepositoryStateManager.rebaseInteractiveState.value
if (
repositoryState == RepositoryState.REBASING_INTERACTIVE &&
rebaseInteractiveState is RebaseInteractiveState.ProcessingCommits &&
rebaseInteractiveState.isCurrentStepAmenable &&
isAmendRebaseInteractive.value
) {
val amendCommitId = getRebaseAmendCommitIdUseCase(git)
if (!amendCommitId.isNullOrBlank()) {
doCommitUseCase(git, message, true, getPersonIdent(git))
}
}
continueRebaseUseCase(git) continueRebaseUseCase(git)
} }
@ -373,6 +411,22 @@ class StatusViewModel @Inject constructor(
_commitMessageChangesFlow.emit(savedCommitMessage.message) _commitMessageChangesFlow.emit(savedCommitMessage.message)
} }
private fun takeMessageFromAmendCommit() = tabState.runOperation(
refreshType = RefreshType.NONE,
) { git ->
val rebaseInteractiveState = rebaseInteractiveState.value
if (rebaseInteractiveState !is RebaseInteractiveState.ProcessingCommits) {
return@runOperation
}
val commitId = rebaseInteractiveState.commitToAmendId ?: return@runOperation
val message = getSpecificCommitMessageUseCase(git, commitId)
savedCommitMessage = savedCommitMessage.copy(message = message)
persistMessage()
_commitMessageChangesFlow.emit(savedCommitMessage.message)
}
fun onRejectCommitterData() { fun onRejectCommitterData() {
this._committerDataRequestState.value = CommitterDataRequestState.Reject this._committerDataRequestState.value = CommitterDataRequestState.Reject
} }

View File

@ -1,11 +1,13 @@
package com.jetpackduba.gitnuro.viewmodels package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.SharedRepositoryStateManager
import com.jetpackduba.gitnuro.credentials.CredentialsAccepted import com.jetpackduba.gitnuro.credentials.CredentialsAccepted
import com.jetpackduba.gitnuro.credentials.CredentialsState import com.jetpackduba.gitnuro.credentials.CredentialsState
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager import com.jetpackduba.gitnuro.credentials.CredentialsStateManager
import com.jetpackduba.gitnuro.git.* import com.jetpackduba.gitnuro.git.*
import com.jetpackduba.gitnuro.git.branches.CreateBranchUseCase import com.jetpackduba.gitnuro.git.branches.CreateBranchUseCase
import com.jetpackduba.gitnuro.git.rebase.AbortRebaseUseCase import com.jetpackduba.gitnuro.git.rebase.GetRebaseInteractiveStateUseCase
import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState
import com.jetpackduba.gitnuro.git.repository.GetRepositoryStateUseCase import com.jetpackduba.gitnuro.git.repository.GetRepositoryStateUseCase
import com.jetpackduba.gitnuro.git.repository.InitLocalRepositoryUseCase import com.jetpackduba.gitnuro.git.repository.InitLocalRepositoryUseCase
import com.jetpackduba.gitnuro.git.repository.OpenRepositoryUseCase import com.jetpackduba.gitnuro.git.repository.OpenRepositoryUseCase
@ -54,7 +56,6 @@ class TabViewModel @Inject constructor(
private val openRepositoryUseCase: OpenRepositoryUseCase, private val openRepositoryUseCase: OpenRepositoryUseCase,
private val openSubmoduleRepositoryUseCase: OpenSubmoduleRepositoryUseCase, private val openSubmoduleRepositoryUseCase: OpenSubmoduleRepositoryUseCase,
private val diffViewModelProvider: Provider<DiffViewModel>, private val diffViewModelProvider: Provider<DiffViewModel>,
private val rebaseInteractiveViewModelProvider: Provider<RebaseInteractiveViewModel>,
private val historyViewModelProvider: Provider<HistoryViewModel>, private val historyViewModelProvider: Provider<HistoryViewModel>,
private val authorViewModelProvider: Provider<AuthorViewModel>, private val authorViewModelProvider: Provider<AuthorViewModel>,
private val tabState: TabState, private val tabState: TabState,
@ -65,9 +66,10 @@ class TabViewModel @Inject constructor(
private val createBranchUseCase: CreateBranchUseCase, private val createBranchUseCase: CreateBranchUseCase,
private val stashChangesUseCase: StashChangesUseCase, private val stashChangesUseCase: StashChangesUseCase,
private val stageUntrackedFileUseCase: StageUntrackedFileUseCase, private val stageUntrackedFileUseCase: StageUntrackedFileUseCase,
private val abortRebaseUseCase: AbortRebaseUseCase,
private val openFilePickerUseCase: OpenFilePickerUseCase, private val openFilePickerUseCase: OpenFilePickerUseCase,
private val openUrlInBrowserUseCase: OpenUrlInBrowserUseCase, private val openUrlInBrowserUseCase: OpenUrlInBrowserUseCase,
private val getRebaseInteractiveStateUseCase: GetRebaseInteractiveStateUseCase,
private val sharedRepositoryStateManager: SharedRepositoryStateManager,
private val tabsManager: TabsManager, private val tabsManager: TabsManager,
private val tabScope: CoroutineScope, private val tabScope: CoroutineScope,
) { ) {
@ -76,13 +78,13 @@ class TabViewModel @Inject constructor(
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
var diffViewModel: DiffViewModel? = null var diffViewModel: DiffViewModel? = null
var rebaseInteractiveViewModel: RebaseInteractiveViewModel? = null
private set
private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None) private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None)
val repositorySelectionStatus: StateFlow<RepositorySelectionStatus> val repositorySelectionStatus: StateFlow<RepositorySelectionStatus>
get() = _repositorySelectionStatus get() = _repositorySelectionStatus
val repositoryState: StateFlow<RepositoryState> = sharedRepositoryStateManager.repositoryState
val rebaseInteractiveState: StateFlow<RebaseInteractiveState> = sharedRepositoryStateManager.rebaseInteractiveState
val processing: StateFlow<ProcessingState> = tabState.processing val processing: StateFlow<ProcessingState> = tabState.processing
val credentialsState: StateFlow<CredentialsState> = credentialsStateManager.credentialsState val credentialsState: StateFlow<CredentialsState> = credentialsStateManager.credentialsState
@ -97,9 +99,6 @@ class TabViewModel @Inject constructor(
updateDiffEntry() updateDiffEntry()
} }
private val _repositoryState = MutableStateFlow(RepositoryState.SAFE)
val repositoryState: StateFlow<RepositoryState> = _repositoryState
private val _blameState = MutableStateFlow<BlameState>(BlameState.None) private val _blameState = MutableStateFlow<BlameState>(BlameState.None)
val blameState: StateFlow<BlameState> = _blameState val blameState: StateFlow<BlameState> = _blameState
@ -123,28 +122,8 @@ class TabViewModel @Inject constructor(
init { init {
tabScope.run { tabScope.run {
launch { launch {
tabState.refreshData.collect { refreshType -> tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REPO_STATE) {
when (refreshType) { loadAuthorInfo(tabState.git)
RefreshType.NONE -> printLog(TAG, "Not refreshing...")
RefreshType.REPO_STATE -> refreshRepositoryState()
else -> {}
}
}
}
launch {
tabState.taskEvent.collect { taskEvent ->
when (taskEvent) {
is TaskEvent.RebaseInteractive -> onRebaseInteractive(taskEvent)
else -> { /*Nothing to do here*/
}
}
}
}
launch {
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REPO_STATE)
{
loadRepositoryState(tabState.git)
} }
} }
@ -156,19 +135,6 @@ class TabViewModel @Inject constructor(
} }
} }
private fun refreshRepositoryState() = tabState.safeProcessing(
refreshType = RefreshType.NONE,
) { git ->
loadRepositoryState(git)
}
private suspend fun onRebaseInteractive(taskEvent: TaskEvent.RebaseInteractive) {
rebaseInteractiveViewModel = rebaseInteractiveViewModelProvider.get()
rebaseInteractiveViewModel?.startRebaseInteractive(taskEvent.revCommit)
rebaseInteractiveViewModel?.onRebaseComplete = {
rebaseInteractiveViewModel = null
}
}
/** /**
* To make sure the tab opens the new repository with a clean state, * To make sure the tab opens the new repository with a clean state,
@ -220,16 +186,6 @@ class TabViewModel @Inject constructor(
} }
} }
private suspend fun loadRepositoryState(git: Git) = withContext(Dispatchers.IO) {
val newRepoState = getRepositoryStateUseCase(git)
printLog(TAG, "Refreshing repository state $newRepoState")
_repositoryState.value = newRepoState
loadAuthorInfo(git)
onRepositoryStateChanged(newRepoState)
}
private fun loadAuthorInfo(git: Git) { private fun loadAuthorInfo(git: Git) {
val config = git.repository.config val config = git.repository.config
config.load() config.load()
@ -250,13 +206,6 @@ class TabViewModel @Inject constructor(
authorViewModel = null authorViewModel = null
} }
private fun onRepositoryStateChanged(newRepoState: RepositoryState) {
if (newRepoState != RepositoryState.REBASING_INTERACTIVE && rebaseInteractiveViewModel != null) {
rebaseInteractiveViewModel?.cancel()
rebaseInteractiveViewModel = null
}
}
private suspend fun watchRepositoryChanges(git: Git) = tabScope.launch(Dispatchers.IO) { private suspend fun watchRepositoryChanges(git: Git) = tabScope.launch(Dispatchers.IO) {
val ignored = git.status().call().ignoredNotInIndex.toList() val ignored = git.status().call().ignoredNotInIndex.toList()
var asyncJob: Job? = null var asyncJob: Job? = null
@ -470,13 +419,6 @@ class TabViewModel @Inject constructor(
Desktop.getDesktop().open(git.repository.workTree) Desktop.getDesktop().open(git.repository.workTree)
} }
fun cancelRebaseInteractive() = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
) { git ->
abortRebaseUseCase(git)
rebaseInteractiveViewModel = null // shouldn't be necessary but just to make sure
}
fun gpgCredentialsAccepted(password: String) { fun gpgCredentialsAccepted(password: String) {
credentialsStateManager.updateState(CredentialsAccepted.GpgCredentialsAccepted(password)) credentialsStateManager.updateState(CredentialsAccepted.GpgCredentialsAccepted(password))
} }