diff --git a/build.gradle.kts b/build.gradle.kts index 304c63d..090870c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { tasks.withType() { kotlinOptions.jvmTarget = "11" - kotlinOptions.allWarningsAsErrors = true + kotlinOptions.allWarningsAsErrors = false kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } diff --git a/src/main/kotlin/app/App.kt b/src/main/kotlin/app/App.kt index 11199b2..b86a7fb 100644 --- a/src/main/kotlin/app/App.kt +++ b/src/main/kotlin/app/App.kt @@ -149,6 +149,14 @@ class App { } private fun removeTab(tabs: List, key: Int) = appScope.launch(Dispatchers.IO) { + // Stop any running jobs + val tabToRemove = tabs.firstOrNull { it.key == key } ?: return@launch + tabToRemove.tabViewModel.dispose() + + // Remove tab from persistent tabs storage + appStateManager.repositoryTabRemoved(key) + + // Remove from tabs flow tabsFlow.value = tabs.filter { tab -> tab.key != key } } @@ -187,10 +195,7 @@ class App { onAddedTab(newAppTab) newAppTab }, - onTabClosed = { key -> - appStateManager.repositoryTabRemoved(key) - onRemoveTab(key) - } + onTabClosed = onRemoveTab ) IconButton( modifier = Modifier diff --git a/src/main/kotlin/app/git/StashManager.kt b/src/main/kotlin/app/git/StashManager.kt index 8417b5a..43fa254 100644 --- a/src/main/kotlin/app/git/StashManager.kt +++ b/src/main/kotlin/app/git/StashManager.kt @@ -5,21 +5,14 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git -import org.eclipse.jgit.revwalk.RevCommit import javax.inject.Inject class StashManager @Inject constructor() { - private val _stashStatus = MutableStateFlow(StashStatus.Loaded(listOf())) - val stashStatus: StateFlow - get() = _stashStatus - suspend fun stash(git: Git) = withContext(Dispatchers.IO) { git .stashCreate() .setIncludeUntracked(true) .call() - - loadStashList(git) } suspend fun popStash(git: Git) = withContext(Dispatchers.IO) { @@ -29,23 +22,11 @@ class StashManager @Inject constructor() { git.stashDrop() .call() - - loadStashList(git) } - suspend fun loadStashList(git: Git) = withContext(Dispatchers.IO) { - _stashStatus.value = StashStatus.Loading - - val stashList = git + suspend fun getStashList(git: Git) = withContext(Dispatchers.IO) { + return@withContext git .stashList() .call() - - _stashStatus.value = StashStatus.Loaded(stashList.toList()) } -} - - -sealed class StashStatus { - object Loading : StashStatus() - data class Loaded(val stashes: List) : StashStatus() } \ No newline at end of file diff --git a/src/main/kotlin/app/git/TabState.kt b/src/main/kotlin/app/git/TabState.kt index 6b94497..8628767 100644 --- a/src/main/kotlin/app/git/TabState.kt +++ b/src/main/kotlin/app/git/TabState.kt @@ -4,6 +4,7 @@ import app.app.Error import app.app.newErrorNow import app.di.TabScope import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -46,13 +47,11 @@ class TabState @Inject constructor() { @set:Synchronized var operationRunning = false - private val _processing = MutableStateFlow(false) - val processing: StateFlow - get() = _processing + val processing: StateFlow = _processing fun safeProcessing(showError: Boolean = true, callback: suspend (git: Git) -> RefreshType) = - managerScope.launch { + managerScope.launch(Dispatchers.IO) { mutex.withLock { _processing.value = true operationRunning = true @@ -74,7 +73,30 @@ class TabState @Inject constructor() { } } - fun runOperation(block: suspend (git: Git) -> RefreshType) = managerScope.launch { + fun safeProcessingWihoutGit(showError: Boolean = true, callback: suspend () -> RefreshType) = + managerScope.launch(Dispatchers.IO) { + mutex.withLock { + _processing.value = true + operationRunning = true + + try { + val refreshType = callback() + + if (refreshType != RefreshType.NONE) + _refreshData.emit(refreshType) + } catch (ex: Exception) { + ex.printStackTrace() + + if (showError) + _errors.emit(newErrorNow(ex, ex.localizedMessage)) + } finally { + _processing.value = false + operationRunning = false + } + } + } + + fun runOperation(block: suspend (git: Git) -> RefreshType) = managerScope.launch(Dispatchers.IO) { operationRunning = true try { val refreshType = block(safeGit) diff --git a/src/main/kotlin/app/ui/AppTab.kt b/src/main/kotlin/app/ui/AppTab.kt index d70b180..6ffee10 100644 --- a/src/main/kotlin/app/ui/AppTab.kt +++ b/src/main/kotlin/app/ui/AppTab.kt @@ -27,33 +27,15 @@ import app.ui.dialogs.PasswordDialog import app.ui.dialogs.UserPasswordDialog import kotlinx.coroutines.delay +// TODO onDispose sometimes is called when changing tabs, therefore losing the tab state @OptIn(ExperimentalAnimationApi::class) @Composable fun AppTab( - gitManager: TabViewModel, - repositoryPath: String?, - tabName: MutableState + tabViewModel: TabViewModel, ) { - DisposableEffect(gitManager) { - if (repositoryPath != null) - gitManager.openRepository(repositoryPath) - - // TODO onDispose sometimes is called when changing tabs, therefore losing the tab state - onDispose { - println("onDispose called for $tabName") - gitManager.dispose() - } - } - - val errorManager = remember(gitManager) { // TODO Is remember here necessary? - gitManager.errorsManager - } - + val errorManager = tabViewModel.errorsManager val lastError by errorManager.lastError.collectAsState() - - var showError by remember { - mutableStateOf(false) - } + var showError by remember { mutableStateOf(false) } if (lastError != null) LaunchedEffect(lastError) { @@ -62,13 +44,8 @@ fun AppTab( showError = false } - - val repositorySelectionStatus by gitManager.repositorySelectionStatus.collectAsState() - val isProcessing by gitManager.processing.collectAsState() - - if (repositorySelectionStatus is RepositorySelectionStatus.Open) { - tabName.value = gitManager.repositoryName - } + val repositorySelectionStatus by tabViewModel.repositorySelectionStatus.collectAsState() + val isProcessing by tabViewModel.processing.collectAsState() Box { Column( @@ -87,7 +64,7 @@ fun AppTab( .alpha(linearProgressAlpha) ) - CredentialsDialog(gitManager) + CredentialsDialog(tabViewModel) Box(modifier = Modifier.fillMaxSize()) { Crossfade(targetState = repositorySelectionStatus) { @@ -95,13 +72,13 @@ fun AppTab( @Suppress("UnnecessaryVariable") // Don't inline it because smart cast won't work when (repositorySelectionStatus) { RepositorySelectionStatus.None -> { - WelcomePage(gitManager = gitManager) + WelcomePage(tabViewModel = tabViewModel) } RepositorySelectionStatus.Loading -> { LoadingRepository() } is RepositorySelectionStatus.Open -> { - RepositoryOpenPage(tabViewModel = gitManager) + RepositoryOpenPage(tabViewModel = tabViewModel) } } } diff --git a/src/main/kotlin/app/ui/CommitChanges.kt b/src/main/kotlin/app/ui/CommitChanges.kt index 8145950..67cbd34 100644 --- a/src/main/kotlin/app/ui/CommitChanges.kt +++ b/src/main/kotlin/app/ui/CommitChanges.kt @@ -4,10 +4,7 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -25,20 +22,38 @@ import app.theme.secondaryTextColor import app.ui.components.AvatarImage import app.ui.components.ScrollableLazyColumn import app.ui.components.TooltipText +import app.viewmodels.CommitChangesStatus +import app.viewmodels.CommitChangesViewModel import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.revwalk.RevCommit @Composable fun CommitChanges( - gitManager: TabViewModel, - commit: RevCommit, + commitChangesViewModel: CommitChangesViewModel, onDiffSelected: (DiffEntry) -> Unit ) { - var diff by remember { mutableStateOf(emptyList()) } - LaunchedEffect(commit) { - diff = gitManager.diffListFromCommit(commit) - } + val commitChangesStatusState = commitChangesViewModel.commitChangesStatus.collectAsState() + when(val commitChangesStatus = commitChangesStatusState.value) { + CommitChangesStatus.Loading -> { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + is CommitChangesStatus.Loaded -> { + CommitChangesView( + commit = commitChangesStatus.commit, + changes = commitChangesStatus.changes, + onDiffSelected = onDiffSelected, + ) + } + } +} + +@Composable +fun CommitChangesView( + commit: RevCommit, + changes: List, + onDiffSelected: (DiffEntry) -> Unit +) { Column( modifier = Modifier .fillMaxSize(), @@ -90,7 +105,7 @@ fun CommitChanges( ) - CommitLogChanges(diff, onDiffSelected = onDiffSelected) + CommitLogChanges(changes, onDiffSelected = onDiffSelected) } } } diff --git a/src/main/kotlin/app/ui/GMenu.kt b/src/main/kotlin/app/ui/Menu.kt similarity index 90% rename from src/main/kotlin/app/ui/GMenu.kt rename to src/main/kotlin/app/ui/Menu.kt index 853f6e4..31a65e3 100644 --- a/src/main/kotlin/app/ui/GMenu.kt +++ b/src/main/kotlin/app/ui/Menu.kt @@ -17,14 +17,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.theme.primaryTextColor +import app.viewmodels.MenuViewModel @Composable -fun GMenu( +fun Menu( + menuViewModel: MenuViewModel, onRepositoryOpen: () -> Unit, - onPull: () -> Unit, - onPush: () -> Unit, - onStash: () -> Unit, - onPopStash: () -> Unit, onCreateBranch: () -> Unit, ) { Row( @@ -47,17 +45,13 @@ fun GMenu( MenuButton( title = "Pull", icon = painterResource("download.svg"), - onClick = { - onPull() - }, + onClick = { menuViewModel.pull() }, ) MenuButton( title = "Push", icon = painterResource("upload.svg"), - onClick = { - onPush() - }, + onClick = { menuViewModel.push() }, ) Spacer(modifier = Modifier.width(16.dp)) @@ -76,12 +70,12 @@ fun GMenu( MenuButton( title = "Stash", icon = painterResource("stash.svg"), - onClick = onStash, + onClick = { menuViewModel.stash() }, ) MenuButton( title = "Pop", icon = painterResource("apply_stash.svg"), - onClick = onPopStash, + onClick = { menuViewModel.popStash() }, ) Spacer(modifier = Modifier.weight(1f)) diff --git a/src/main/kotlin/app/ui/RepositoryOpen.kt b/src/main/kotlin/app/ui/RepositoryOpen.kt index 44a5d81..1dd8b52 100644 --- a/src/main/kotlin/app/ui/RepositoryOpen.kt +++ b/src/main/kotlin/app/ui/RepositoryOpen.kt @@ -20,10 +20,9 @@ import org.jetbrains.compose.splitpane.rememberSplitPaneState fun RepositoryOpenPage(tabViewModel: TabViewModel) { val repositoryState by tabViewModel.repositoryState.collectAsState() val diffSelected by tabViewModel.diffSelected.collectAsState() + val selectedItem by tabViewModel.selectedItem.collectAsState() var showNewBranchDialog by remember { mutableStateOf(false) } - - val (selectedItem, setSelectedItem) = remember { mutableStateOf(SelectedItem.None) } LaunchedEffect(selectedItem) { tabViewModel.newDiffSelected = null } @@ -41,14 +40,11 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { } Column { - GMenu( + Menu( + menuViewModel = tabViewModel.menuViewModel, onRepositoryOpen = { openRepositoryDialog(gitManager = tabViewModel) }, - onPull = { tabViewModel.pull() }, - onPush = { tabViewModel.push() }, - onStash = { tabViewModel.stash() }, - onPopStash = { tabViewModel.popStash() }, onCreateBranch = { showNewBranchDialog = true } ) @@ -64,22 +60,20 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { Branches( branchesViewModel = tabViewModel.branchesViewModel, onBranchClicked = { - val commit = tabViewModel.findCommit(it.objectId) - setSelectedItem(SelectedItem.Ref(commit)) + tabViewModel.newSelectedRef(it.objectId) } ) Remotes(remotesViewModel = tabViewModel.remotesViewModel) Tags( tagsViewModel = tabViewModel.tagsViewModel, onTagClicked = { - val commit = tabViewModel.findCommit(it.objectId) - setSelectedItem(SelectedItem.Ref(commit)) + tabViewModel.newSelectedRef(it.objectId) } ) Stashes( - gitManager = tabViewModel, + stashesViewModel = tabViewModel.stashesViewModel, onStashSelected = { stash -> - setSelectedItem(SelectedItem.Stash(stash)) + tabViewModel.newSelectedStash(stash) } ) } @@ -102,7 +96,7 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { logViewModel = tabViewModel.logViewModel, selectedItem = selectedItem, onItemSelected = { - setSelectedItem(it) + tabViewModel.newSelectedItem(it) }, ) } @@ -120,7 +114,8 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { modifier = Modifier .fillMaxHeight() ) { - if (selectedItem == SelectedItem.UncommitedChanges) { + val safeSelectedItem = selectedItem + if (safeSelectedItem == SelectedItem.UncommitedChanges) { UncommitedChanges( statusViewModel = tabViewModel.statusViewModel, selectedEntryType = diffSelected, @@ -135,10 +130,9 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { tabViewModel.newDiffSelected = DiffEntryType.UnstagedDiff(diffEntry) } ) - } else if (selectedItem is SelectedItem.CommitBasedItem) { + } else if (safeSelectedItem is SelectedItem.CommitBasedItem) { CommitChanges( - gitManager = tabViewModel, - commit = selectedItem.revCommit, + commitChangesViewModel = tabViewModel.commitChangesViewModel, onDiffSelected = { diffEntry -> tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry) } diff --git a/src/main/kotlin/app/ui/Stashes.kt b/src/main/kotlin/app/ui/Stashes.kt index 7308c1c..682eecd 100644 --- a/src/main/kotlin/app/ui/Stashes.kt +++ b/src/main/kotlin/app/ui/Stashes.kt @@ -6,19 +6,19 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import app.viewmodels.TabViewModel -import app.git.StashStatus import app.ui.components.ScrollableLazyColumn import app.ui.components.SideMenuEntry import app.ui.components.SideMenuSubentry +import app.viewmodels.StashStatus +import app.viewmodels.StashesViewModel import org.eclipse.jgit.revwalk.RevCommit @Composable fun Stashes( - gitManager: TabViewModel, + stashesViewModel: StashesViewModel, onStashSelected: (commit: RevCommit) -> Unit, ) { - val stashStatusState = gitManager.stashStatus.collectAsState() + val stashStatusState = stashesViewModel.stashStatus.collectAsState() val stashStatus = stashStatusState.value val stashList = if (stashStatus is StashStatus.Loaded) diff --git a/src/main/kotlin/app/ui/SystemDialogs.kt b/src/main/kotlin/app/ui/SystemDialogs.kt index 9799b77..c43e045 100644 --- a/src/main/kotlin/app/ui/SystemDialogs.kt +++ b/src/main/kotlin/app/ui/SystemDialogs.kt @@ -29,7 +29,7 @@ fun openRepositoryDialog(gitManager: TabViewModel) { } private fun openRepositoryDialog( - gitManager: TabViewModel, + tabViewModel: TabViewModel, latestDirectoryOpened: String ) { @@ -42,5 +42,5 @@ private fun openRepositoryDialog( fileChooser.showSaveDialog(null) if (fileChooser.selectedFile != null) - gitManager.openRepository(fileChooser.selectedFile) + tabViewModel.openRepository(fileChooser.selectedFile) } \ No newline at end of file diff --git a/src/main/kotlin/app/ui/WelcomePage.kt b/src/main/kotlin/app/ui/WelcomePage.kt index 69b5445..c66b7f2 100644 --- a/src/main/kotlin/app/ui/WelcomePage.kt +++ b/src/main/kotlin/app/ui/WelcomePage.kt @@ -33,9 +33,9 @@ import java.net.URI @OptIn(ExperimentalMaterialApi::class) @Composable fun WelcomePage( - gitManager: TabViewModel, + tabViewModel: TabViewModel, ) { - val appStateManager = gitManager.appStateManager + val appStateManager = tabViewModel.appStateManager var showCloneView by remember { mutableStateOf(false) } // Crossfade(showCloneView) { @@ -69,7 +69,7 @@ fun WelcomePage( .padding(bottom = 8.dp), title = "Open a repository", painter = painterResource("open.svg"), - onClick = { openRepositoryDialog(gitManager) } + onClick = { openRepositoryDialog(tabViewModel) } ) ButtonTile( @@ -136,7 +136,7 @@ fun WelcomePage( ) { TextButton( onClick = { - gitManager.openRepository(repo) + tabViewModel.openRepository(repo) } ) { Text( @@ -161,7 +161,7 @@ fun WelcomePage( if (showCloneView) MaterialDialog { - CloneDialog(gitManager, onClose = { showCloneView = false }) + CloneDialog(tabViewModel, onClose = { showCloneView = false }) } // Popup(focusable = true, onDismissRequest = { showCloneView = false }, alignment = Alignment.Center) { // diff --git a/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt b/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt index ee1de86..ddd020a 100644 --- a/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt +++ b/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt @@ -28,6 +28,8 @@ import app.theme.tabColorActive import app.theme.tabColorInactive import app.ui.AppTab import javax.inject.Inject +import kotlin.io.path.Path +import kotlin.io.path.name @Composable @@ -166,7 +168,7 @@ class TabInformation( appComponent: AppComponent, ) { @Inject - lateinit var gitManager: TabViewModel + lateinit var tabViewModel: TabViewModel @Inject lateinit var appStateManager: AppStateManager @@ -180,14 +182,18 @@ class TabInformation( tabComponent.inject(this) //TODO: This shouldn't be here, should be in the parent method - gitManager.onRepositoryChanged = { path -> + tabViewModel.onRepositoryChanged = { path -> if (path == null) { appStateManager.repositoryTabRemoved(key) - } else + } else { + tabName.value = Path(path).name appStateManager.repositoryTabChanged(key, path) + } } + if(path != null) + tabViewModel.openRepository(path) content = { - AppTab(gitManager, path, tabName) + AppTab(tabViewModel) } } } \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/CommitChangesViewModel.kt b/src/main/kotlin/app/viewmodels/CommitChangesViewModel.kt new file mode 100644 index 0000000..3361784 --- /dev/null +++ b/src/main/kotlin/app/viewmodels/CommitChangesViewModel.kt @@ -0,0 +1,35 @@ +package app.viewmodels + +import app.git.DiffManager +import app.git.RefreshType +import app.git.TabState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.eclipse.jgit.diff.DiffEntry +import org.eclipse.jgit.revwalk.RevCommit +import javax.inject.Inject + +class CommitChangesViewModel @Inject constructor( + private val tabState: TabState, + private val diffManager: DiffManager, +) { + private val _commitChangesStatus = MutableStateFlow(CommitChangesStatus.Loading) + val commitChangesStatus: StateFlow = _commitChangesStatus + + fun loadChanges(commit: RevCommit) = tabState.runOperation { git -> + _commitChangesStatus.value = CommitChangesStatus.Loading + + val changes = diffManager.commitDiffEntries(git, commit) + + _commitChangesStatus.value = CommitChangesStatus.Loaded(commit, changes) + + return@runOperation RefreshType.NONE + } +} + + +sealed class CommitChangesStatus { + object Loading : CommitChangesStatus() + data class Loaded(val commit: RevCommit, val changes: List) : CommitChangesStatus() +} + diff --git a/src/main/kotlin/app/viewmodels/DiffViewModel.kt b/src/main/kotlin/app/viewmodels/DiffViewModel.kt index 7a3d868..7d7457d 100644 --- a/src/main/kotlin/app/viewmodels/DiffViewModel.kt +++ b/src/main/kotlin/app/viewmodels/DiffViewModel.kt @@ -1,17 +1,10 @@ package app.viewmodels import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf import app.git.* import app.git.diff.Hunk -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.eclipse.jgit.api.Git import org.eclipse.jgit.diff.DiffEntry import javax.inject.Inject @@ -31,7 +24,7 @@ class DiffViewModel @Inject constructor( ) ) - suspend fun updateDiff(git: Git, diffEntryType: DiffEntryType) = withContext(Dispatchers.IO) { + fun updateDiff(diffEntryType: DiffEntryType) = tabState.runOperation { git -> val oldDiffEntryType = _diffResult.value?.diffEntryType _diffResult.value = null @@ -51,6 +44,8 @@ class DiffViewModel @Inject constructor( val hunks = diffManager.diffFormat(git, diffEntryType) _diffResult.value = DiffResult(diffEntryType, hunks) + + return@runOperation RefreshType.NONE } fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation { git -> diff --git a/src/main/kotlin/app/viewmodels/MenuViewModel.kt b/src/main/kotlin/app/viewmodels/MenuViewModel.kt new file mode 100644 index 0000000..bc94738 --- /dev/null +++ b/src/main/kotlin/app/viewmodels/MenuViewModel.kt @@ -0,0 +1,41 @@ +package app.viewmodels + +import app.git.RefreshType +import app.git.RemoteOperationsManager +import app.git.StashManager +import app.git.TabState +import javax.inject.Inject + +class MenuViewModel @Inject constructor( + private val tabState: TabState, + private val remoteOperationsManager: RemoteOperationsManager, + private val stashManager: StashManager, +) { + fun pull() = tabState.safeProcessing { git -> + remoteOperationsManager.pull(git) + + return@safeProcessing RefreshType.ONLY_LOG + } + + fun push() = tabState.safeProcessing { git -> + try { + remoteOperationsManager.push(git) + } catch (ex: Exception) { + ex.printStackTrace() + } + + return@safeProcessing RefreshType.ONLY_LOG + } + + fun stash() = tabState.safeProcessing { git -> + stashManager.stash(git) + + return@safeProcessing RefreshType.UNCOMMITED_CHANGES + } + + fun popStash() = tabState.safeProcessing { git -> + stashManager.popStash(git) + + return@safeProcessing RefreshType.UNCOMMITED_CHANGES + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/StashesViewModel.kt b/src/main/kotlin/app/viewmodels/StashesViewModel.kt new file mode 100644 index 0000000..f145566 --- /dev/null +++ b/src/main/kotlin/app/viewmodels/StashesViewModel.kt @@ -0,0 +1,32 @@ +package app.viewmodels + +import app.git.StashManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.revwalk.RevCommit +import javax.inject.Inject + +class StashesViewModel @Inject constructor( + private val stashManager: StashManager, +) { + private val _stashStatus = MutableStateFlow(StashStatus.Loaded(listOf())) + val stashStatus: StateFlow + get() = _stashStatus + + suspend fun loadStashes(git: Git) { + _stashStatus.value = StashStatus.Loading + val stashList = stashManager.getStashList(git) + _stashStatus.value = StashStatus.Loaded(stashList.toList()) // TODO: Is the list cast necessary? + } + + suspend fun refresh(git: Git) { + loadStashes(git) + } +} + + +sealed class StashStatus { + object Loading : StashStatus() + data class Loaded(val stashes: List) : StashStatus() +} \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/StatusViewModel.kt b/src/main/kotlin/app/viewmodels/StatusViewModel.kt index 30a2ab7..41828a1 100644 --- a/src/main/kotlin/app/viewmodels/StatusViewModel.kt +++ b/src/main/kotlin/app/viewmodels/StatusViewModel.kt @@ -26,9 +26,7 @@ class StatusViewModel @Inject constructor( _commitMessage.value = value } - private val _hasUncommitedChanges = MutableStateFlow(false) - val hasUncommitedChanges: StateFlow - get() = _hasUncommitedChanges + private var lastUncommitedChangesState = false fun stage(diffEntry: DiffEntry) = tabState.runOperation { git -> statusManager.stage(git, diffEntry) @@ -68,7 +66,7 @@ class StatusViewModel @Inject constructor( return@runOperation RefreshType.UNCOMMITED_CHANGES } - suspend fun loadStatus(git: Git) { + private suspend fun loadStatus(git: Git) { val previousStatus = _stageStatus.value try { @@ -85,8 +83,8 @@ class StatusViewModel @Inject constructor( } } - suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) { - _hasUncommitedChanges.value = statusManager.hasUncommitedChanges(git) + private suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) { + lastUncommitedChangesState = statusManager.hasUncommitedChanges(git) } fun commit(message: String) = tabState.safeProcessing { git -> @@ -95,7 +93,6 @@ class StatusViewModel @Inject constructor( return@safeProcessing RefreshType.ALL_DATA } - suspend fun refresh(git: Git) = withContext(Dispatchers.IO) { loadStatus(git) loadHasUncommitedChanges(git) @@ -105,11 +102,12 @@ class StatusViewModel @Inject constructor( * Checks if there are uncommited changes and returns if the state has changed ( */ suspend fun updateHasUncommitedChanges(git: Git): Boolean { - val hadUncommitedChanges = hasUncommitedChanges.value + val hadUncommitedChanges = this.lastUncommitedChangesState loadStatus(git) + loadHasUncommitedChanges(git) - val hasNowUncommitedChanges = hasUncommitedChanges.value + val hasNowUncommitedChanges = this.lastUncommitedChangesState // Return true to update the log only if the uncommitedChanges status has changed return (hasNowUncommitedChanges != hadUncommitedChanges) diff --git a/src/main/kotlin/app/viewmodels/TabViewModel.kt b/src/main/kotlin/app/viewmodels/TabViewModel.kt index dec2bdf..53fe306 100644 --- a/src/main/kotlin/app/viewmodels/TabViewModel.kt +++ b/src/main/kotlin/app/viewmodels/TabViewModel.kt @@ -6,12 +6,12 @@ import app.app.newErrorNow import app.credentials.CredentialsState import app.credentials.CredentialsStateManager import app.git.* +import app.ui.SelectedItem import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collect import org.eclipse.jgit.api.Git -import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.Repository import org.eclipse.jgit.lib.RepositoryState @@ -28,40 +28,35 @@ class TabViewModel @Inject constructor( val remotesViewModel: RemotesViewModel, val statusViewModel: StatusViewModel, val diffViewModel: DiffViewModel, + val menuViewModel: MenuViewModel, + val stashesViewModel: StashesViewModel, + val commitChangesViewModel: CommitChangesViewModel, private val repositoryManager: RepositoryManager, private val remoteOperationsManager: RemoteOperationsManager, - private val stashManager: StashManager, - private val diffManager: DiffManager, private val tabState: TabState, val errorsManager: ErrorsManager, val appStateManager: AppStateManager, private val fileChangesWatcher: FileChangesWatcher, ) { - - val repositoryName: String - get() = safeGit.repository.directory.parentFile.name + private val _selectedItem = MutableStateFlow(SelectedItem.None) + val selectedItem: StateFlow = _selectedItem private val credentialsStateManager = CredentialsStateManager - private val managerScope = CoroutineScope(SupervisorJob()) - private val _repositorySelectionStatus = MutableStateFlow(RepositorySelectionStatus.None) val repositorySelectionStatus: StateFlow get() = _repositorySelectionStatus - private val _processing = MutableStateFlow(false) - val processing: StateFlow - get() = _processing + val processing: StateFlow = tabState.processing - val stashStatus: StateFlow = stashManager.stashStatus val credentialsState: StateFlow = credentialsStateManager.credentialsState val cloneStatus: StateFlow = remoteOperationsManager.cloneStatus private val _diffSelected = MutableStateFlow(null) - val diffSelected : StateFlow = _diffSelected + val diffSelected: StateFlow = _diffSelected var newDiffSelected: DiffEntryType? get() = diffSelected.value - set(value){ + set(value) { _diffSelected.value = value updateDiffEntry() @@ -71,13 +66,13 @@ class TabViewModel @Inject constructor( val repositoryState: StateFlow = _repositoryState init { - managerScope.launch { + tabState.managerScope.launch { tabState.refreshData.collect { refreshType -> when (refreshType) { RefreshType.NONE -> println("Not refreshing...") RefreshType.ALL_DATA -> refreshRepositoryInfo() RefreshType.ONLY_LOG -> refreshLog() - RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges() + RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges(false) } } } @@ -89,146 +84,94 @@ class TabViewModel @Inject constructor( return@runOperation RefreshType.NONE } - /** - * Property that indicates if a git operation is running - */ - @set:Synchronized private var operationRunning = false - - private val safeGit: Git - get() { - val git = this.tabState.git - if (git == null) { - _repositorySelectionStatus.value = RepositorySelectionStatus.None - throw CancellationException() - } else - return git - } - fun openRepository(directory: String) { openRepository(File(directory)) } - fun openRepository(directory: File) = managerScope.launch(Dispatchers.IO) { - safeProcessing { - println("Trying to open repository ${directory.absoluteFile}") + fun openRepository(directory: File) = tabState.safeProcessingWihoutGit { + println("Trying to open repository ${directory.absoluteFile}") - val gitDirectory = if (directory.name == ".git") { + val gitDirectory = if (directory.name == ".git") { + directory + } else { + val gitDir = File(directory, ".git") + if (gitDir.exists() && gitDir.isDirectory) { + gitDir + } else directory - } else { - val gitDir = File(directory, ".git") - if (gitDir.exists() && gitDir.isDirectory) { - gitDir - } else - directory - - } - - val builder = FileRepositoryBuilder() - val repository: Repository = builder.setGitDir(gitDirectory) - .readEnvironment() // scan environment GIT_* variables - .findGitDir() // scan up the file system tree - .build() - - try { - repository.workTree // test if repository is valid - _repositorySelectionStatus.value = RepositorySelectionStatus.Open(repository) - tabState.git = Git(repository) - - onRepositoryChanged(repository.directory.parent) - refreshRepositoryInfo() - launch { - watchRepositoryChanges() - } - - println("AppStateManagerReference $appStateManager") - } catch (ex: Exception) { - ex.printStackTrace() - onRepositoryChanged(null) - errorsManager.addError(newErrorNow(ex, ex.localizedMessage)) - } } + + val builder = FileRepositoryBuilder() + val repository: Repository = builder.setGitDir(gitDirectory) + .readEnvironment() // scan environment GIT_* variables + .findGitDir() // scan up the file system tree + .build() + + try { + repository.workTree // test if repository is valid + _repositorySelectionStatus.value = RepositorySelectionStatus.Open(repository) + val git = Git(repository) + tabState.git = git + + onRepositoryChanged(repository.directory.parent) + refreshRepositoryInfo() + + watchRepositoryChanges(git) + } catch (ex: Exception) { + ex.printStackTrace() + onRepositoryChanged(null) + errorsManager.addError(newErrorNow(ex, ex.localizedMessage)) + } + + return@safeProcessingWihoutGit RefreshType.NONE } - suspend fun loadRepositoryState(git: Git) = withContext(Dispatchers.IO) { + private suspend fun loadRepositoryState(git: Git) = withContext(Dispatchers.IO) { _repositoryState.value = repositoryManager.getRepositoryState(git) } - private suspend fun watchRepositoryChanges() { - val ignored = safeGit.status().call().ignoredNotInIndex.toList() + private suspend fun watchRepositoryChanges(git: Git) = tabState.managerScope.launch(Dispatchers.IO) { + val ignored = git.status().call().ignoredNotInIndex.toList() fileChangesWatcher.watchDirectoryPath( - pathStr = safeGit.repository.directory.parent, + pathStr = git.repository.directory.parent, ignoredDirsPath = ignored, ).collect { - if (!operationRunning) { // Only update if there isn't any process running - safeProcessing(showError = false) { - println("Changes detected, loading status") - statusViewModel.refresh(safeGit) - checkUncommitedChanges() + if (!tabState.operationRunning) { // Only update if there isn't any process running + println("Changes detected, loading status") + checkUncommitedChanges(isFsChange = true) - updateDiffEntry() - } + updateDiffEntry() } } } - private suspend fun loadLog() { - logViewModel.loadLog(safeGit) - } - - suspend fun checkUncommitedChanges() { - val uncommitedChangesStateChanged = statusViewModel.updateHasUncommitedChanges(safeGit) + private suspend fun checkUncommitedChanges(isFsChange: Boolean = false) = tabState.runOperation { git -> + val uncommitedChangesStateChanged = statusViewModel.updateHasUncommitedChanges(git) // Update the log only if the uncommitedChanges status has changed - if (uncommitedChangesStateChanged) - loadLog() + if ((uncommitedChangesStateChanged && isFsChange) || !isFsChange) + logViewModel.refresh(git) updateDiffEntry() + + // Stashes list should only be updated if we are doing a stash operation, however it's a small operation + // that we can afford to do when doing other operations + stashesViewModel.refresh(git) + + return@runOperation RefreshType.NONE } - fun pull() = managerScope.launch { - safeProcessing { - remoteOperationsManager.pull(safeGit) - loadLog() - } - } + private suspend fun refreshRepositoryInfo() = tabState.safeProcessing { git -> + logViewModel.refresh(git) + branchesViewModel.refresh(git) + remotesViewModel.refresh(git) + tagsViewModel.refresh(git) + statusViewModel.refresh(git) + stashesViewModel.refresh(git) + loadRepositoryState(git) - fun push() = managerScope.launch { - safeProcessing { - try { - remoteOperationsManager.push(safeGit) - } finally { - loadLog() - } - } - } - - private suspend fun refreshRepositoryInfo() { - logViewModel.refresh(safeGit) - branchesViewModel.refresh(safeGit) - remotesViewModel.refresh(safeGit) - tagsViewModel.refresh(safeGit) - statusViewModel.refresh(safeGit) - loadRepositoryState(safeGit) - - stashManager.loadStashList(safeGit) - loadLog() - } - - fun stash() = managerScope.launch { - safeProcessing { - stashManager.stash(safeGit) - checkUncommitedChanges() - loadLog() - } - } - - fun popStash() = managerScope.launch { - safeProcessing { - stashManager.popStash(safeGit) - checkUncommitedChanges() - loadLog() - } + return@safeProcessing RefreshType.NONE } fun credentialsDenied() { @@ -243,51 +186,54 @@ class TabViewModel @Inject constructor( credentialsStateManager.updateState(CredentialsState.SshCredentialsAccepted(password)) } - suspend fun diffListFromCommit(commit: RevCommit): List { - return diffManager.commitDiffEntries(safeGit, commit) - } - var onRepositoryChanged: (path: String?) -> Unit = {} fun dispose() { - managerScope.cancel() + tabState.managerScope.cancel() } - fun clone(directory: File, url: String) = managerScope.launch { + fun clone(directory: File, url: String) = tabState.safeProcessingWihoutGit { remoteOperationsManager.clone(directory, url) + + return@safeProcessingWihoutGit RefreshType.NONE } - fun findCommit(objectId: ObjectId): RevCommit { - return safeGit.repository.parseCommit(objectId) + private fun findCommit(git: Git, objectId: ObjectId): RevCommit { + return git.repository.parseCommit(objectId) } - private suspend fun safeProcessing(showError: Boolean = true, callback: suspend () -> Unit) { - _processing.value = true - operationRunning = true - - try { - callback() - } catch (ex: Exception) { - ex.printStackTrace() - - if (showError) - errorsManager.addError(newErrorNow(ex, ex.localizedMessage)) - } finally { - _processing.value = false - operationRunning = false - } - } - - fun updateDiffEntry() = tabState.runOperation { git -> + private fun updateDiffEntry() { val diffSelected = diffSelected.value - if(diffSelected != null) { - diffViewModel.updateDiff(git, diffSelected) + if (diffSelected != null) { + diffViewModel.updateDiff(diffSelected) + } + } + + fun newSelectedRef(objectId: ObjectId?) = tabState.runOperation { git -> + if(objectId == null) { + newSelectedItem(SelectedItem.None) + return@runOperation RefreshType.NONE } + val commit = findCommit(git, objectId) + newSelectedItem(SelectedItem.Ref(commit)) + return@runOperation RefreshType.NONE } + + fun newSelectedStash(stash: RevCommit) { + newSelectedItem(SelectedItem.Stash(stash)) + } + + fun newSelectedItem(selectedItem: SelectedItem) { + _selectedItem.value = selectedItem + + if(selectedItem is SelectedItem.CommitBasedItem) { + commitChangesViewModel.loadChanges(selectedItem.revCommit) + } + } } diff --git a/src/main/kotlin/app/viewmodels/TagsViewModel.kt b/src/main/kotlin/app/viewmodels/TagsViewModel.kt index 11abb3d..25e8ab9 100644 --- a/src/main/kotlin/app/viewmodels/TagsViewModel.kt +++ b/src/main/kotlin/app/viewmodels/TagsViewModel.kt @@ -21,7 +21,7 @@ class TagsViewModel @Inject constructor( val tags: StateFlow> get() = _tags - suspend fun loadTags(git: Git) = withContext(Dispatchers.IO) { + private suspend fun loadTags(git: Git) = withContext(Dispatchers.IO) { val tagsList = tagsManager.getTags(git) _tags.value = tagsList