Implemented context menu for stash operations

Moved selected item to TabState, so every ViewModel can update the current selected tab state without having to use callbacks to the RepoOpened component. This allows to set currently selected item to "None" when droping a stash that has been selected
This commit is contained in:
Abdelilah El Aissaoui 2022-02-06 22:57:46 +01:00
parent fff18b7fef
commit 02313fe632
19 changed files with 225 additions and 80 deletions

View File

@ -3,6 +3,7 @@ package app.git
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
class StashManager @Inject constructor() {
@ -22,9 +23,29 @@ class StashManager @Inject constructor() {
.call()
}
suspend fun popStash(git: Git, stash: RevCommit) = withContext(Dispatchers.IO) {
applyStash(git, stash)
deleteStash(git, stash)
}
suspend fun getStashList(git: Git) = withContext(Dispatchers.IO) {
return@withContext git
.stashList()
.call()
}
suspend fun applyStash(git: Git, stashInfo: RevCommit) = withContext(Dispatchers.IO) {
git.stashApply()
.setStashRef(stashInfo.name)
.call()
}
suspend fun deleteStash(git: Git, stashInfo: RevCommit) = withContext(Dispatchers.IO) {
val stashList = getStashList(git)
val indexOfStashToDelete = stashList.indexOf(stashInfo)
git.stashDrop()
.setStashRef(indexOfStashToDelete)
.call()
}
}

View File

@ -281,10 +281,8 @@ class StatusManager @Inject constructor(
suspend fun getStatusSummary(git: Git, currentBranch: Ref?, repositoryState: RepositoryState): StatusSummary {
val staged = getStaged(git, currentBranch, repositoryState)
val allChanges = staged.toMutableList()
println("Staged: $staged")
val unstaged = getUnstaged(git, repositoryState)
println("Unstaged: $unstaged")
allChanges.addAll(unstaged)
val groupedChanges = allChanges.groupBy {

View File

@ -3,6 +3,7 @@ package app.git
import app.ErrorsManager
import app.di.TabScope
import app.newErrorNow
import app.ui.SelectedItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -14,6 +15,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException
@ -21,12 +24,14 @@ import kotlin.coroutines.cancellation.CancellationException
class TabState @Inject constructor(
val errorsManager: ErrorsManager,
) {
private val _selectedItem = MutableStateFlow<SelectedItem>(SelectedItem.None)
val selectedItem: StateFlow<SelectedItem> = _selectedItem
var git: Git? = null
val safeGit: Git
get() {
val git = this.git
if (git == null) {
// _repositorySelectionStatus.value = RepositorySelectionStatus.None
throw CancellationException("Null git object")
} else
return git
@ -128,11 +133,44 @@ class TabState @Inject constructor(
_refreshData.emit(refreshType)
}
}
fun newSelectedStash(stash: RevCommit) {
newSelectedItem(SelectedItem.Stash(stash))
}
fun noneSelected() {
newSelectedItem(SelectedItem.None)
}
fun newSelectedRef(objectId: ObjectId?) = runOperation(
refreshType = RefreshType.NONE,
) { git ->
if (objectId == null) {
newSelectedItem(SelectedItem.None)
} else {
val commit = findCommit(git, objectId)
newSelectedItem(SelectedItem.Ref(commit))
}
}
private fun findCommit(git: Git, objectId: ObjectId): RevCommit {
return git.repository.parseCommit(objectId)
}
fun newSelectedItem(selectedItem: SelectedItem) {
_selectedItem.value = selectedItem
println(selectedItem)
// if (selectedItem is SelectedItem.CommitBasedItem) {
// commitChangesViewModel.loadChanges(selectedItem.revCommit)
// }
}
}
enum class RefreshType {
NONE,
ALL_DATA,
ONLY_LOG,
STASHES,
UNCOMMITED_CHANGES,
UNCOMMITED_CHANGES_AND_LOG,
}

View File

@ -22,7 +22,6 @@ import org.eclipse.jgit.lib.Ref
@Composable
fun Branches(
branchesViewModel: BranchesViewModel,
onBranchClicked: (Ref) -> Unit,
) {
val branches by branchesViewModel.branches.collectAsState()
val currentBranch by branchesViewModel.currentBranch.collectAsState()
@ -37,7 +36,7 @@ fun Branches(
BranchLineEntry(
branch = branch,
isCurrentBranch = currentBranch == branch.name,
onBranchClicked = { onBranchClicked(branch) },
onBranchClicked = { branchesViewModel.selectBranch(branch) },
onCheckoutBranch = { branchesViewModel.checkoutRef(branch) },
onMergeBranch = { setMergeBranch(branch) },
onDeleteBranch = { branchesViewModel.deleteBranch(branch) },

View File

@ -8,10 +8,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
@ -34,8 +31,13 @@ import org.eclipse.jgit.revwalk.RevCommit
@Composable
fun CommitChanges(
commitChangesViewModel: CommitChangesViewModel,
onDiffSelected: (DiffEntry) -> Unit
onDiffSelected: (DiffEntry) -> Unit,
selectedItem: SelectedItem.CommitBasedItem
) {
LaunchedEffect(selectedItem) {
commitChangesViewModel.loadChanges(selectedItem.revCommit)
}
val commitChangesStatusState = commitChangesViewModel.commitChangesStatus.collectAsState()
when (val commitChangesStatus = commitChangesStatusState.value) {

View File

@ -21,7 +21,6 @@ import org.eclipse.jgit.lib.Ref
@Composable
fun Remotes(
remotesViewModel: RemotesViewModel,
onBranchClicked: (Ref) -> Unit,
) {
val remotes by remotesViewModel.remotes.collectAsState()
@ -43,7 +42,7 @@ fun Remotes(
itemContent = { remoteInfo ->
RemoteRow(
remote = remoteInfo,
onBranchClicked = { branch -> onBranchClicked(branch) },
onBranchClicked = { branch -> remotesViewModel.selectBranch(branch) },
onDeleteBranch = { branch -> remotesViewModel.deleteRemoteBranch(branch) },
onRemoteClicked = { remotesViewModel.onRemoteClicked(remoteInfo) }
)

View File

@ -51,7 +51,7 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
)
Row {
HorizontalSplitPane() {
HorizontalSplitPane {
first(minSize = 200.dp) {
Column(
modifier = Modifier
@ -61,27 +61,15 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
) {
Branches(
branchesViewModel = tabViewModel.branchesViewModel,
onBranchClicked = {
tabViewModel.newSelectedRef(it.objectId)
}
)
Remotes(
remotesViewModel = tabViewModel.remotesViewModel,
onBranchClicked = {
tabViewModel.newSelectedRef(it.objectId)
}
)
Tags(
tagsViewModel = tabViewModel.tagsViewModel,
onTagClicked = {
tabViewModel.newSelectedRef(it.objectId)
}
)
Stashes(
stashesViewModel = tabViewModel.stashesViewModel,
onStashSelected = { stash ->
tabViewModel.newSelectedStash(stash)
}
)
}
}
@ -104,9 +92,6 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
Log(
logViewModel = tabViewModel.logViewModel,
selectedItem = selectedItem,
onItemSelected = {
tabViewModel.newSelectedItem(it)
},
repositoryState = repositoryState,
)
}
@ -143,6 +128,7 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
} else if (safeSelectedItem is SelectedItem.CommitBasedItem) {
CommitChanges(
commitChangesViewModel = tabViewModel.commitChangesViewModel,
selectedItem = safeSelectedItem,
onDiffSelected = { diffEntry ->
tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry)
}

View File

@ -1,10 +1,16 @@
@file:OptIn(ExperimentalFoundationApi::class)
package app.ui
import androidx.compose.foundation.ContextMenuArea
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.res.painterResource
import app.ui.components.SideMenuPanel
import app.ui.components.SideMenuSubentry
import app.ui.context_menu.stashesContextMenuItems
import app.viewmodels.StashStatus
import app.viewmodels.StashesViewModel
import org.eclipse.jgit.revwalk.RevCommit
@ -12,7 +18,6 @@ import org.eclipse.jgit.revwalk.RevCommit
@Composable
fun Stashes(
stashesViewModel: StashesViewModel,
onStashSelected: (commit: RevCommit) -> Unit,
) {
val stashStatusState = stashesViewModel.stashStatus.collectAsState()
val stashStatus = stashStatusState.value
@ -22,15 +27,23 @@ fun Stashes(
else
listOf()
SideMenuPanel(
title = "Stashes",
icon = painterResource("stash.svg"),
items = stashList,
itemContent = { stashInfo ->
itemContent = { stash ->
StashRow(
stash = stashInfo,
onClick = { onStashSelected(stashInfo) }
stash = stash,
onClick = { stashesViewModel.selectTab(stash) },
contextItems = stashesContextMenuItems(
onApply = { stashesViewModel.applyStash(stash) },
onPop = {
stashesViewModel.popStash(stash)
},
onDelete = {
stashesViewModel.deleteStash(stash)
},
)
)
}
)
@ -38,10 +51,18 @@ fun Stashes(
}
@Composable
private fun StashRow(stash: RevCommit, onClick: () -> Unit) {
SideMenuSubentry(
text = stash.shortMessage,
iconResourcePath = "stash.svg",
onClick = onClick,
)
private fun StashRow(
stash: RevCommit,
onClick: () -> Unit,
contextItems: List<ContextMenuItem>,
) {
ContextMenuArea(
items = { contextItems }
) {
SideMenuSubentry(
text = stash.shortMessage,
iconResourcePath = "stash.svg",
onClick = onClick,
)
}
}

View File

@ -15,7 +15,6 @@ import org.eclipse.jgit.lib.Ref
@Composable
fun Tags(
tagsViewModel: TagsViewModel,
onTagClicked: (Ref) -> Unit,
) {
val tagsState = tagsViewModel.tags.collectAsState()
val tags = tagsState.value
@ -27,7 +26,7 @@ fun Tags(
itemContent = { tag ->
TagRow(
tag = tag,
onTagClicked = { onTagClicked(tag) },
onTagClicked = { tagsViewModel.selectTag(tag) },
onCheckoutTag = { tagsViewModel.checkoutRef(tag) },
onDeleteTag = { tagsViewModel.deleteTag(tag) }
)

View File

@ -0,0 +1,26 @@
package app.ui.context_menu
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi
@OptIn(ExperimentalFoundationApi::class)
fun stashesContextMenuItems(
onApply: () -> Unit,
onPop: () -> Unit,
onDelete: () -> Unit,
): List<ContextMenuItem> {
return mutableListOf(
ContextMenuItem(
label = "Apply stash",
onClick = onApply
),
ContextMenuItem(
label = "Pop stash",
onClick = onPop
),
ContextMenuItem(
label = "Drop stash",
onClick = onDelete
),
)
}

View File

@ -72,7 +72,6 @@ private const val CANVAS_MIN_WIDTH = 100
fun Log(
logViewModel: LogViewModel,
selectedItem: SelectedItem,
onItemSelected: (SelectedItem) -> Unit,
repositoryState: RepositoryState,
) {
val logStatusState = logViewModel.logStatus.collectAsState()
@ -114,7 +113,6 @@ fun Log(
.background(MaterialTheme.colors.background)
.fillMaxSize()
) {
// val hasUncommitedChanges by tabViewModel.hasUncommitedChanges.collectAsState()
val weightMod = remember { mutableStateOf(0f) }
var graphWidth = (CANVAS_MIN_WIDTH + weightMod.value).dp
@ -131,7 +129,6 @@ fun Log(
.background(MaterialTheme.colors.background)
.fillMaxSize(),
) {
//TODO: Shouldn't this be an item of the graph?
if (hasUncommitedChanges)
item {
UncommitedChangesLine(
@ -142,7 +139,7 @@ fun Log(
weightMod = weightMod,
repositoryState = repositoryState,
onUncommitedChangesSelected = {
onItemSelected(SelectedItem.UncommitedChanges)
logViewModel.selectLogLine(SelectedItem.UncommitedChanges)
}
)
}
@ -160,7 +157,7 @@ fun Log(
onMergeBranch = { ref -> showLogDialog.value = LogDialog.MergeBranch(ref) },
onRebaseBranch = { ref -> showLogDialog.value = LogDialog.RebaseBranch(ref) },
onRevCommitSelected = {
onItemSelected(SelectedItem.Commit(graphNode))
logViewModel.selectLogLine(SelectedItem.Commit(graphNode))
}
)
}

View File

@ -71,4 +71,8 @@ class BranchesViewModel @Inject constructor(
) { git ->
rebaseManager.rebaseBranch(git, ref)
}
fun selectBranch(ref: Ref) {
tabState.newSelectedRef(ref.objectId)
}
}

View File

@ -2,6 +2,7 @@ package app.viewmodels
import app.git.*
import app.git.graph.GraphCommitList
import app.ui.SelectedItem
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.eclipse.jgit.api.Git
@ -143,6 +144,10 @@ class LogViewModel @Inject constructor(
) { git ->
rebaseManager.rebaseBranch(git, ref)
}
fun selectLogLine(selectedItem: SelectedItem) {
tabState.newSelectedItem(selectedItem)
}
}
sealed class LogStatus {

View File

@ -34,13 +34,13 @@ class MenuViewModel @Inject constructor(
}
fun stash() = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITED_CHANGES,
refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG,
) { git ->
stashManager.stash(git)
}
fun popStash() = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITED_CHANGES,
refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG,
) { git ->
stashManager.popStash(git)
}

View File

@ -58,6 +58,10 @@ class RemotesViewModel @Inject constructor(
_remotes.value = newRemotesList
}
fun selectBranch(ref: Ref) {
tabState.newSelectedRef(ref.objectId)
}
}
data class RemoteView(val remoteInfo: RemoteInfo, val isExpanded: Boolean)

View File

@ -1,6 +1,9 @@
package app.viewmodels
import app.git.RefreshType
import app.git.StashManager
import app.git.TabState
import app.ui.SelectedItem
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.eclipse.jgit.api.Git
@ -9,6 +12,7 @@ import javax.inject.Inject
class StashesViewModel @Inject constructor(
private val stashManager: StashManager,
private val tabState: TabState,
) {
private val _stashStatus = MutableStateFlow<StashStatus>(StashStatus.Loaded(listOf()))
val stashStatus: StateFlow<StashStatus>
@ -23,6 +27,41 @@ class StashesViewModel @Inject constructor(
suspend fun refresh(git: Git) {
loadStashes(git)
}
fun applyStash(stashInfo: RevCommit) = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG,
) { git ->
stashManager.applyStash(git, stashInfo)
}
fun popStash(stash: RevCommit) = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG,
) { git ->
stashManager.popStash(git, stash)
stashDropped(stash)
}
fun deleteStash(stash: RevCommit) = tabState.safeProcessing(
refreshType = RefreshType.STASHES,
) { git ->
stashManager.deleteStash(git, stash)
stashDropped(stash)
}
fun selectTab(stash: RevCommit) {
tabState.newSelectedStash(stash)
}
private fun stashDropped(stash: RevCommit) {
val selectedValue = tabState.selectedItem.value
if (
selectedValue is SelectedItem.Stash &&
selectedValue.revCommit.name == stash.name
) {
tabState.noneSelected()
}
}
}

View File

@ -108,6 +108,22 @@ class StatusViewModel @Inject constructor(
loadHasUncommitedChanges(git)
}
/**
* Checks if there are uncommited changes and returns if the state has changed (
*/
suspend fun updateHasUncommitedChanges(git: Git): Boolean {
val hadUncommitedChanges = this.lastUncommitedChangesState
loadStatus(git)
loadHasUncommitedChanges(git)
val hasNowUncommitedChanges = this.lastUncommitedChangesState
hasPreviousCommits = logManager.hasPreviousCommits(git)
// Return true to update the log only if the uncommitedChanges status has changed
return (hasNowUncommitedChanges != hadUncommitedChanges)
}
fun continueRebase() = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
) { git ->

View File

@ -41,8 +41,7 @@ class TabViewModel @Inject constructor(
private val fileChangesWatcher: FileChangesWatcher,
) {
val errorsManager: ErrorsManager = tabState.errorsManager
private val _selectedItem = MutableStateFlow<SelectedItem>(SelectedItem.None)
val selectedItem: StateFlow<SelectedItem> = _selectedItem
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
private val credentialsStateManager = CredentialsStateManager
@ -75,12 +74,20 @@ class TabViewModel @Inject constructor(
RefreshType.NONE -> println("Not refreshing...")
RefreshType.ALL_DATA -> refreshRepositoryInfo()
RefreshType.ONLY_LOG -> refreshLog()
RefreshType.STASHES -> refreshStashes()
RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges()
RefreshType.UNCOMMITED_CHANGES_AND_LOG -> checkUncommitedChanges(true)
}
}
}
}
private fun refreshStashes() = tabState.runOperation(
refreshType = RefreshType.NONE
) { git ->
stashesViewModel.refresh(git)
}
private fun refreshLog() = tabState.runOperation(
refreshType = RefreshType.NONE,
) { git ->
@ -147,11 +154,18 @@ class TabViewModel @Inject constructor(
}
}
private suspend fun checkUncommitedChanges() = tabState.runOperation(
private suspend fun checkUncommitedChanges(fullUpdateLog: Boolean = false) = tabState.runOperation(
refreshType = RefreshType.NONE,
) { git ->
statusViewModel.refresh(git)
logViewModel.refreshUncommitedChanges(git)
val uncommitedChangesStateChanged = statusViewModel.updateHasUncommitedChanges(git)
println("Has uncommitedChangesStateChanged $uncommitedChangesStateChanged")
// Update the log only if the uncommitedChanges status has changed or requested
if (uncommitedChangesStateChanged || fullUpdateLog)
logViewModel.refresh(git)
else
logViewModel.refreshUncommitedChanges(git)
updateDiffEntry()
@ -195,10 +209,6 @@ class TabViewModel @Inject constructor(
remoteOperationsManager.clone(directory, url)
}
private fun findCommit(git: Git, objectId: ObjectId): RevCommit {
return git.repository.parseCommit(objectId)
}
private fun updateDiffEntry() {
val diffSelected = diffSelected.value
@ -206,29 +216,6 @@ class TabViewModel @Inject constructor(
diffViewModel.updateDiff(diffSelected)
}
}
fun newSelectedRef(objectId: ObjectId?) = tabState.runOperation(
refreshType = RefreshType.NONE,
) { git ->
if (objectId == null) {
newSelectedItem(SelectedItem.None)
} else {
val commit = findCommit(git, objectId)
newSelectedItem(SelectedItem.Ref(commit))
}
}
fun newSelectedStash(stash: RevCommit) {
newSelectedItem(SelectedItem.Stash(stash))
}
fun newSelectedItem(selectedItem: SelectedItem) {
_selectedItem.value = selectedItem
if (selectedItem is SelectedItem.CommitBasedItem) {
commitChangesViewModel.loadChanges(selectedItem.revCommit)
}
}
}

View File

@ -39,6 +39,10 @@ class TagsViewModel @Inject constructor(
tagsManager.deleteTag(git, tag)
}
fun selectTag(tag: Ref) {
tabState.newSelectedRef(tag.objectId)
}
suspend fun refresh(git: Git) {
loadTags(git)
}