From 9c53ce726eefcb42adc339a6e635b36b213747c3 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Mon, 3 Jan 2022 21:39:53 +0100 Subject: [PATCH 1/5] Creation of multiple viewmodels that hold data state --- build.gradle.kts | 2 +- src/main/kotlin/app/App.kt | 26 +- src/main/kotlin/app/di/AppComponent.kt | 2 + src/main/kotlin/app/di/TabComponent.kt | 10 + src/main/kotlin/app/di/TabScope.kt | 7 + src/main/kotlin/app/git/BranchesManager.kt | 29 +- src/main/kotlin/app/git/LogManager.kt | 34 +-- src/main/kotlin/app/git/RemotesManager.kt | 8 +- src/main/kotlin/app/git/RepositoryManager.kt | 12 + src/main/kotlin/app/git/StatusManager.kt | 156 ++++------- src/main/kotlin/app/git/TabState.kt | 100 +++++++ src/main/kotlin/app/git/TagsManager.kt | 12 +- src/main/kotlin/app/ui/AppTab.kt | 8 +- src/main/kotlin/app/ui/Branches.kt | 18 +- src/main/kotlin/app/ui/CommitChanges.kt | 6 +- src/main/kotlin/app/ui/Diff.kt | 25 +- src/main/kotlin/app/ui/Remotes.kt | 7 +- src/main/kotlin/app/ui/RepositoryOpen.kt | 55 ++-- src/main/kotlin/app/ui/Stashes.kt | 4 +- src/main/kotlin/app/ui/SystemDialogs.kt | 6 +- src/main/kotlin/app/ui/Tags.kt | 11 +- src/main/kotlin/app/ui/UncommitedChanges.kt | 32 +-- src/main/kotlin/app/ui/WelcomePage.kt | 4 +- .../app/ui/components/RepositoriesTabPanel.kt | 38 ++- src/main/kotlin/app/ui/dialogs/CloneDialog.kt | 4 +- src/main/kotlin/app/ui/log/Log.kt | 46 ++-- .../app/viewmodels/BranchesViewModel.kt | 60 +++++ .../kotlin/app/viewmodels/DiffViewModel.kt | 44 +++ .../kotlin/app/viewmodels/LogViewModel.kt | 97 +++++++ .../kotlin/app/viewmodels/RemotesViewModel.kt | 44 +++ .../kotlin/app/viewmodels/StatusViewModel.kt | 124 +++++++++ .../TabViewModel.kt} | 255 +++++------------- .../kotlin/app/viewmodels/TagsViewModel.kt | 45 ++++ 33 files changed, 846 insertions(+), 485 deletions(-) create mode 100644 src/main/kotlin/app/di/TabComponent.kt create mode 100644 src/main/kotlin/app/di/TabScope.kt create mode 100644 src/main/kotlin/app/git/RepositoryManager.kt create mode 100644 src/main/kotlin/app/git/TabState.kt create mode 100644 src/main/kotlin/app/viewmodels/BranchesViewModel.kt create mode 100644 src/main/kotlin/app/viewmodels/DiffViewModel.kt create mode 100644 src/main/kotlin/app/viewmodels/LogViewModel.kt create mode 100644 src/main/kotlin/app/viewmodels/RemotesViewModel.kt create mode 100644 src/main/kotlin/app/viewmodels/StatusViewModel.kt rename src/main/kotlin/app/{git/GitManager.kt => viewmodels/TabViewModel.kt} (53%) create mode 100644 src/main/kotlin/app/viewmodels/TagsViewModel.kt diff --git a/build.gradle.kts b/build.gradle.kts index 5f5d373..304c63d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,7 +44,7 @@ tasks.withType() { compose.desktop { application { mainClass = "MainKt" - +// nativeDistributions { includeAllModules = true targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.AppImage) diff --git a/src/main/kotlin/app/App.kt b/src/main/kotlin/app/App.kt index eb8fd4a..c0ac860 100644 --- a/src/main/kotlin/app/App.kt +++ b/src/main/kotlin/app/App.kt @@ -24,21 +24,15 @@ import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.zIndex import app.di.DaggerAppComponent -import app.git.GitManager import app.theme.AppTheme -import app.ui.AppTab import app.ui.components.RepositoriesTabPanel import app.ui.components.TabInformation import app.ui.dialogs.SettingsDialog import javax.inject.Inject -import javax.inject.Provider class Main { private val appComponent = DaggerAppComponent.create() - @Inject - lateinit var gitManagerProvider: Provider - @Inject lateinit var appStateManager: AppStateManager @@ -47,7 +41,7 @@ class Main { init { appComponent.inject(this) - + println("AppStateManagerReference $appStateManager") appStateManager.loadRepositoriesTabs() } @@ -200,19 +194,11 @@ class Main { ): TabInformation { return TabInformation( - title = tabName, - key = key - ) { - val gitManager = remember { gitManagerProvider.get() } - gitManager.onRepositoryChanged = { path -> - if (path == null) { - appStateManager.repositoryTabRemoved(key) - } else - appStateManager.repositoryTabChanged(key, path) - } - - AppTab(gitManager, path, tabName) - } + tabName = tabName, + key = key, + path = path, + appComponent = appComponent, + ) } } diff --git a/src/main/kotlin/app/di/AppComponent.kt b/src/main/kotlin/app/di/AppComponent.kt index 97578d8..19b6a2c 100644 --- a/src/main/kotlin/app/di/AppComponent.kt +++ b/src/main/kotlin/app/di/AppComponent.kt @@ -1,5 +1,6 @@ package app.di +import app.AppStateManager import app.Main import dagger.Component import javax.inject.Singleton @@ -8,4 +9,5 @@ import javax.inject.Singleton @Component interface AppComponent { fun inject(main: Main) + fun appStateManager(): AppStateManager } \ No newline at end of file diff --git a/src/main/kotlin/app/di/TabComponent.kt b/src/main/kotlin/app/di/TabComponent.kt new file mode 100644 index 0000000..0d7e172 --- /dev/null +++ b/src/main/kotlin/app/di/TabComponent.kt @@ -0,0 +1,10 @@ +package app.di + +import app.ui.components.TabInformation +import dagger.Component + +@TabScope +@Component(dependencies = [ AppComponent::class ]) +interface TabComponent { + fun inject(tabInformation: TabInformation) +} \ No newline at end of file diff --git a/src/main/kotlin/app/di/TabScope.kt b/src/main/kotlin/app/di/TabScope.kt new file mode 100644 index 0000000..0ca01d8 --- /dev/null +++ b/src/main/kotlin/app/di/TabScope.kt @@ -0,0 +1,7 @@ +package app.di + +import javax.inject.Scope + +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class TabScope \ No newline at end of file diff --git a/src/main/kotlin/app/git/BranchesManager.kt b/src/main/kotlin/app/git/BranchesManager.kt index 039fa33..757b0f2 100644 --- a/src/main/kotlin/app/git/BranchesManager.kt +++ b/src/main/kotlin/app/git/BranchesManager.kt @@ -1,9 +1,12 @@ package app.git +import app.extensions.isBranch +import app.extensions.simpleName import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.CreateBranchCommand import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.ListBranchCommand import org.eclipse.jgit.api.MergeCommand @@ -32,17 +35,6 @@ class BranchesManager @Inject constructor() { return branchList.firstOrNull { it.name == branchName } } - suspend fun loadBranches(git: Git) = withContext(Dispatchers.IO) { - val branchList = getBranches(git) - - val branchName = git - .repository - .fullBranch - - _branches.value = branchList - _currentBranch.value = branchName - } - suspend fun getBranches(git: Git) = withContext(Dispatchers.IO) { return@withContext git .branchList() @@ -55,8 +47,6 @@ class BranchesManager @Inject constructor() { .setCreateBranch(true) .setName(branchName) .call() - - loadBranches(git) } suspend fun createBranchOnCommit(git: Git, branch: String, revCommit: RevCommit) = withContext(Dispatchers.IO) { @@ -95,4 +85,17 @@ class BranchesManager @Inject constructor() { .setListMode(ListBranchCommand.ListMode.REMOTE) .call() } + + suspend fun checkoutRef(git: Git, ref: Ref) = withContext(Dispatchers.IO) { + git.checkout().apply { + setName(ref.name) + if (ref.isBranch && ref.name.startsWith("refs/remotes/")) { + setCreateBranch(true) + setName(ref.simpleName) + setStartPoint(ref.objectId.name) + setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK) + } + call() + } + } } \ No newline at end of file diff --git a/src/main/kotlin/app/git/LogManager.kt b/src/main/kotlin/app/git/LogManager.kt index 9bdb025..9b0aecb 100644 --- a/src/main/kotlin/app/git/LogManager.kt +++ b/src/main/kotlin/app/git/LogManager.kt @@ -21,17 +21,8 @@ import javax.inject.Inject class LogManager @Inject constructor( private val statusManager: StatusManager, - private val branchesManager: BranchesManager, ) { - private val _logStatus = MutableStateFlow(LogStatus.Loading) - - val logStatus: StateFlow - get() = _logStatus - - suspend fun loadLog(git: Git) = withContext(Dispatchers.IO) { - _logStatus.value = LogStatus.Loading - - val currentBranch = branchesManager.currentBranchRef(git) + suspend fun loadLog(git: Git, currentBranch: Ref?) = withContext(Dispatchers.IO) { val commitList = GraphCommitList() val repositoryState = git.repository.repositoryState @@ -45,7 +36,7 @@ class LogManager @Inject constructor( walk.markStartAllRefs(Constants.R_REMOTES) walk.markStartAllRefs(Constants.R_TAGS) - if (statusManager.checkHasUncommitedChanges(git)) + if (statusManager.hasUncommitedChanges(git)) commitList.addUncommitedChangesGraphCommit(logList.first()) commitList.source(walk) @@ -55,9 +46,8 @@ class LogManager @Inject constructor( ensureActive() } - val loadedStatus = LogStatus.Loaded(commitList, currentBranch) - _logStatus.value = loadedStatus + return@withContext commitList } suspend fun checkoutCommit(git: Git, revCommit: RevCommit) = withContext(Dispatchers.IO) { @@ -67,19 +57,6 @@ class LogManager @Inject constructor( .call() } - suspend fun checkoutRef(git: Git, ref: Ref) = withContext(Dispatchers.IO) { - git.checkout().apply { - setName(ref.name) - if (ref.isBranch && ref.name.startsWith("refs/remotes/")) { - setCreateBranch(true) - setName(ref.simpleName) - setStartPoint(ref.objectId.name) - setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK) - } - call() - } - } - suspend fun revertCommit(git: Git, revCommit: RevCommit) = withContext(Dispatchers.IO) { git .revert() @@ -106,9 +83,4 @@ enum class ResetType { SOFT, MIXED, HARD, -} - -sealed class LogStatus { - object Loading : LogStatus() - class Loaded(val plotCommitList: GraphCommitList, val currentBranch: Ref?) : LogStatus() } \ No newline at end of file diff --git a/src/main/kotlin/app/git/RemotesManager.kt b/src/main/kotlin/app/git/RemotesManager.kt index fc6ef8a..afbe4a9 100644 --- a/src/main/kotlin/app/git/RemotesManager.kt +++ b/src/main/kotlin/app/git/RemotesManager.kt @@ -10,22 +10,18 @@ import org.eclipse.jgit.transport.RemoteConfig import javax.inject.Inject class RemotesManager @Inject constructor() { - private val _remotes = MutableStateFlow>(listOf()) - val remotes: StateFlow> - get() = _remotes + suspend fun loadRemotes(git: Git, allRemoteBranches: List) = withContext(Dispatchers.IO) { val remotes = git.remoteList() .call() - val remoteInfoList = remotes.map { remoteConfig -> + return@withContext remotes.map { remoteConfig -> val remoteBranches = allRemoteBranches.filter { branch -> branch.name.startsWith("refs/remotes/${remoteConfig.name}") } RemoteInfo(remoteConfig, remoteBranches) } - - _remotes.value = remoteInfoList } } diff --git a/src/main/kotlin/app/git/RepositoryManager.kt b/src/main/kotlin/app/git/RepositoryManager.kt new file mode 100644 index 0000000..27df2a9 --- /dev/null +++ b/src/main/kotlin/app/git/RepositoryManager.kt @@ -0,0 +1,12 @@ +package app.git + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import javax.inject.Inject + +class RepositoryManager @Inject constructor() { + suspend fun getRepositoryState(git: Git) = withContext(Dispatchers.IO) { + return@withContext git.repository.repositoryState + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/StatusManager.kt b/src/main/kotlin/app/git/StatusManager.kt index d6d8cee..17a9d11 100644 --- a/src/main/kotlin/app/git/StatusManager.kt +++ b/src/main/kotlin/app/git/StatusManager.kt @@ -12,7 +12,6 @@ import app.git.diff.Hunk import app.git.diff.LineType import app.theme.conflictFile import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext @@ -31,24 +30,9 @@ import javax.inject.Inject class StatusManager @Inject constructor( - private val branchesManager: BranchesManager, private val rawFileManagerFactory: RawFileManagerFactory, ) { - private val _stageStatus = MutableStateFlow(StageStatus.Loaded(listOf(), listOf())) - val stageStatus: StateFlow = _stageStatus - - private val _repositoryState = MutableStateFlow(RepositoryState.SAFE) - val repositoryState: StateFlow = _repositoryState - - private val _hasUncommitedChanges = MutableStateFlow(false) - val hasUncommitedChanges: StateFlow - get() = _hasUncommitedChanges - - suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) { - _hasUncommitedChanges.value = checkHasUncommitedChanges(git) - } - - suspend fun checkHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) { + suspend fun hasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) { val status = git .status() .call() @@ -56,77 +40,6 @@ class StatusManager @Inject constructor( return@withContext status.hasUncommittedChanges() || status.hasUntrackedChanges() } - suspend fun loadRepositoryStatus(git: Git) = withContext(Dispatchers.IO) { - _repositoryState.value = git.repository.repositoryState - } - - suspend fun loadStatus(git: Git) = withContext(Dispatchers.IO) { - val previousStatus = _stageStatus.value - _stageStatus.value = StageStatus.Loading - - try { - loadRepositoryStatus(git) - - loadHasUncommitedChanges(git) - val currentBranch = branchesManager.currentBranchRef(git) - val repositoryState = git.repository.repositoryState - - val staged = git - .diff() - .setShowNameAndStatusOnly(true).apply { - if (currentBranch == null && !repositoryState.isMerging && !repositoryState.isRebasing) - setOldTree(EmptyTreeIterator()) // Required if the repository is empty - - setCached(true) - } - .call() - // TODO: Grouping and fitlering allows us to remove duplicates when conflicts appear, requires more testing (what happens in windows? /dev/null is a unix thing) - // TODO: Test if we should group by old path or new path - .groupBy { - if(it.newPath != "/dev/null") - it.newPath - else - it.oldPath - } - .map { - val entries = it.value - - val hasConflicts = - (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing)) - - StatusEntry(entries.first(), isConflict = hasConflicts) - } - - ensureActive() - - val unstaged = git - .diff() - .setShowNameAndStatusOnly(true) - .call() - .groupBy { - if(it.oldPath != "/dev/null") - it.oldPath - else - it.newPath - } - .map { - val entries = it.value - - val hasConflicts = - (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing)) - - StatusEntry(entries.first(), isConflict = hasConflicts) - } - - ensureActive() - _stageStatus.value = StageStatus.Loaded(staged, unstaged) - } catch (ex: Exception) { - _stageStatus.value = previousStatus - throw ex - } - - } - suspend fun stage(git: Git, diffEntry: DiffEntry) = withContext(Dispatchers.IO) { if (diffEntry.changeType == DiffEntry.ChangeType.DELETE) { git.rm() @@ -137,8 +50,6 @@ class StatusManager @Inject constructor( .addFilepattern(diffEntry.filePath) .call() } - - loadStatus(git) } suspend fun stageHunk(git: Git, diffEntry: DiffEntry, hunk: Hunk) = withContext(Dispatchers.IO) { @@ -175,7 +86,7 @@ class StatusManager @Inject constructor( completedWithErrors = false - loadStatus(git) +// loadStatus(git) } finally { if (completedWithErrors) dirCache.unlock() @@ -226,7 +137,7 @@ class StatusManager @Inject constructor( completedWithErrors = false - loadStatus(git) +// loadStatus(git) } finally { if (completedWithErrors) dirCache.unlock() @@ -271,8 +182,6 @@ class StatusManager @Inject constructor( git.reset() .addPath(diffEntry.filePath) .call() - - loadStatus(git) } suspend fun commit(git: Git, message: String) = withContext(Dispatchers.IO) { @@ -280,8 +189,6 @@ class StatusManager @Inject constructor( .setMessage(message) .setAllowEmpty(false) .call() - - loadStatus(git) } suspend fun reset(git: Git, diffEntry: DiffEntry, staged: Boolean) = withContext(Dispatchers.IO) { @@ -297,15 +204,13 @@ class StatusManager @Inject constructor( .addPath(diffEntry.filePath) .call() - loadStatus(git) +// loadStatus(git) } suspend fun unstageAll(git: Git) = withContext(Dispatchers.IO) { git .reset() .call() - - loadStatus(git) } suspend fun stageAll(git: Git) = withContext(Dispatchers.IO) { @@ -313,15 +218,58 @@ class StatusManager @Inject constructor( .add() .addFilepattern(".") .call() + } - loadStatus(git) + suspend fun getStaged(git: Git, currentBranch: Ref?, repositoryState: RepositoryState) = withContext(Dispatchers.IO) { + return@withContext git + .diff() + .setShowNameAndStatusOnly(true).apply { + if (currentBranch == null && !repositoryState.isMerging && !repositoryState.isRebasing) + setOldTree(EmptyTreeIterator()) // Required if the repository is empty + + setCached(true) + } + .call() + // TODO: Grouping and fitlering allows us to remove duplicates when conflicts appear, requires more testing (what happens in windows? /dev/null is a unix thing) + // TODO: Test if we should group by old path or new path + .groupBy { + if(it.newPath != "/dev/null") + it.newPath + else + it.oldPath + } + .map { + val entries = it.value + + val hasConflicts = + (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing)) + + StatusEntry(entries.first(), isConflict = hasConflicts) + } + } + + suspend fun getUnstaged(git: Git, repositoryState: RepositoryState) = withContext(Dispatchers.IO) { + return@withContext git + .diff() + .setShowNameAndStatusOnly(true) + .call() + .groupBy { + if(it.oldPath != "/dev/null") + it.oldPath + else + it.newPath + } + .map { + val entries = it.value + + val hasConflicts = + (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing)) + + StatusEntry(entries.first(), isConflict = hasConflicts) + } } } -sealed class StageStatus { - object Loading : StageStatus() - data class Loaded(val staged: List, val unstaged: List) : StageStatus() -} data class StatusEntry(val diffEntry: DiffEntry, val isConflict: Boolean) { val icon: ImageVector diff --git a/src/main/kotlin/app/git/TabState.kt b/src/main/kotlin/app/git/TabState.kt new file mode 100644 index 0000000..6b94497 --- /dev/null +++ b/src/main/kotlin/app/git/TabState.kt @@ -0,0 +1,100 @@ +package app.git + +import app.app.Error +import app.app.newErrorNow +import app.di.TabScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.eclipse.jgit.api.Git +import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException + +@TabScope +class TabState @Inject constructor() { + 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 + } + + val mutex = Mutex() + + private val _refreshData = MutableSharedFlow() + val refreshData: Flow = _refreshData + suspend fun refreshData(refreshType: RefreshType) = _refreshData.emit(refreshType) + + private val _errors = MutableSharedFlow() + val errors: Flow = _errors + val managerScope = CoroutineScope(SupervisorJob()) + + + /** + * Property that indicates if a git operation is running + */ + @set:Synchronized + var operationRunning = false + + + private val _processing = MutableStateFlow(false) + val processing: StateFlow + get() = _processing + + fun safeProcessing(showError: Boolean = true, callback: suspend (git: Git) -> RefreshType) = + managerScope.launch { + mutex.withLock { + _processing.value = true + operationRunning = true + + try { + val refreshType = callback(safeGit) + + 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 { + operationRunning = true + try { + val refreshType = block(safeGit) + + if (refreshType != RefreshType.NONE) + _refreshData.emit(refreshType) + } finally { + operationRunning = false + } + } +} + +enum class RefreshType { + NONE, + ALL_DATA, + ONLY_LOG, + + /** + * Requires to update the status if currently selected and update the log if there has been a change + * in the "uncommited changes" state (if there were changes before but not anymore and vice-versa) + */ + UNCOMMITED_CHANGES, +} \ No newline at end of file diff --git a/src/main/kotlin/app/git/TagsManager.kt b/src/main/kotlin/app/git/TagsManager.kt index 41fa547..12646d3 100644 --- a/src/main/kotlin/app/git/TagsManager.kt +++ b/src/main/kotlin/app/git/TagsManager.kt @@ -10,16 +10,8 @@ import org.eclipse.jgit.revwalk.RevCommit import javax.inject.Inject class TagsManager @Inject constructor() { - - private val _tags = MutableStateFlow>(listOf()) - val tags: StateFlow> - get() = _tags - - suspend fun loadTags(git: Git) = withContext(Dispatchers.IO) { - val branchList = git.tagList().call() - - - _tags.value = branchList + suspend fun getTags(git: Git) = withContext(Dispatchers.IO) { + return@withContext git.tagList().call() } suspend fun createTagOnCommit(git: Git, tag: String, revCommit: RevCommit) = withContext(Dispatchers.IO) { diff --git a/src/main/kotlin/app/ui/AppTab.kt b/src/main/kotlin/app/ui/AppTab.kt index 0275e50..b240b9a 100644 --- a/src/main/kotlin/app/ui/AppTab.kt +++ b/src/main/kotlin/app/ui/AppTab.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.LoadingRepository import app.credentials.CredentialsState -import app.git.GitManager +import app.git.TabViewModel import app.git.RepositorySelectionStatus import app.ui.dialogs.PasswordDialog import app.ui.dialogs.UserPasswordDialog @@ -30,7 +30,7 @@ import kotlinx.coroutines.delay @OptIn(ExperimentalAnimationApi::class) @Composable fun AppTab( - gitManager: GitManager, + gitManager: TabViewModel, repositoryPath: String?, tabName: MutableState ) { @@ -101,7 +101,7 @@ fun AppTab( LoadingRepository() } is RepositorySelectionStatus.Open -> { - RepositoryOpenPage(gitManager = gitManager) + RepositoryOpenPage(tabViewModel = gitManager) } } } @@ -158,7 +158,7 @@ fun AppTab( } @Composable -fun CredentialsDialog(gitManager: GitManager) { +fun CredentialsDialog(gitManager: TabViewModel) { val credentialsState by gitManager.credentialsState.collectAsState() if (credentialsState == CredentialsState.HttpCredentialsRequested) { diff --git a/src/main/kotlin/app/ui/Branches.kt b/src/main/kotlin/app/ui/Branches.kt index 82b0822..5ff12d4 100644 --- a/src/main/kotlin/app/ui/Branches.kt +++ b/src/main/kotlin/app/ui/Branches.kt @@ -13,23 +13,23 @@ import androidx.compose.ui.unit.dp import app.MAX_SIDE_PANEL_ITEMS_HEIGHT import app.extensions.isLocal import app.extensions.simpleName -import app.git.GitManager +import app.git.TabViewModel import app.ui.components.ScrollableLazyColumn import app.ui.components.SideMenuEntry import app.ui.components.SideMenuSubentry import app.ui.components.entryHeight import app.ui.context_menu.branchContextMenuItems import app.ui.dialogs.MergeDialog +import app.viewmodels.BranchesViewModel import org.eclipse.jgit.lib.Ref @Composable fun Branches( - gitManager: GitManager, + branchesViewModel: BranchesViewModel, onBranchClicked: (Ref) -> Unit, - - ) { - val branches by gitManager.branches.collectAsState() - val currentBranch by gitManager.currentBranch.collectAsState() +) { + val branches by branchesViewModel.branches.collectAsState() + val currentBranch by branchesViewModel.currentBranch.collectAsState() val (mergeBranch, setMergeBranch) = remember { mutableStateOf(null) } Column { @@ -48,9 +48,9 @@ fun Branches( branch = branch, isCurrentBranch = currentBranch == branch.name, onBranchClicked = { onBranchClicked(branch) }, - onCheckoutBranch = { gitManager.checkoutRef(branch) }, + onCheckoutBranch = { branchesViewModel.checkoutRef(branch) }, onMergeBranch = { setMergeBranch(branch) }, - onDeleteBranch = { gitManager.deleteBranch(branch) }, + onDeleteBranch = { branchesViewModel.deleteBranch(branch) }, ) } } @@ -62,7 +62,7 @@ fun Branches( currentBranch, mergeBranchName = mergeBranch.name, onReject = { setMergeBranch(null) }, - onAccept = { ff -> gitManager.mergeBranch(mergeBranch, ff) } + onAccept = { ff -> branchesViewModel.mergeBranch(mergeBranch, ff) } ) } } diff --git a/src/main/kotlin/app/ui/CommitChanges.kt b/src/main/kotlin/app/ui/CommitChanges.kt index 76144a9..193af5b 100644 --- a/src/main/kotlin/app/ui/CommitChanges.kt +++ b/src/main/kotlin/app/ui/CommitChanges.kt @@ -3,7 +3,6 @@ package app.ui import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.Divider import androidx.compose.material.Icon @@ -12,14 +11,13 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.extensions.* -import app.git.GitManager +import app.git.TabViewModel import app.theme.headerBackground import app.theme.headerText import app.theme.primaryTextColor @@ -32,7 +30,7 @@ import org.eclipse.jgit.revwalk.RevCommit @Composable fun CommitChanges( - gitManager: GitManager, + gitManager: TabViewModel, commit: RevCommit, onDiffSelected: (DiffEntry) -> Unit ) { diff --git a/src/main/kotlin/app/ui/Diff.kt b/src/main/kotlin/app/ui/Diff.kt index e6dbd3a..f0786cd 100644 --- a/src/main/kotlin/app/ui/Diff.kt +++ b/src/main/kotlin/app/ui/Diff.kt @@ -16,24 +16,25 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.git.DiffEntryType -import app.git.GitManager +import app.git.TabViewModel import app.git.diff.Hunk import app.git.diff.LineType import app.theme.primaryTextColor import app.ui.components.ScrollableLazyColumn import app.ui.components.SecondaryButton +import app.viewmodels.DiffViewModel import org.eclipse.jgit.diff.DiffEntry @Composable -fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView: () -> Unit) { - var text by remember { mutableStateOf(listOf()) } +fun Diff( + diffViewModel: DiffViewModel, + onCloseDiffView: () -> Unit, +) { + val diffResultState = diffViewModel.diffResult.collectAsState() + val diffResult = diffResultState.value ?: return - LaunchedEffect(Unit) { - text = gitManager.diffFormat(diffEntryType) - - - if (text.isEmpty()) onCloseDiffView() - } + val diffEntryType = diffResult.diffEntryType + val hunks = diffResult.hunks Column( modifier = Modifier @@ -59,7 +60,7 @@ fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView: .fillMaxSize() // .padding(16.dp) ) { - itemsIndexed(text) { index, hunk -> + itemsIndexed(hunks) { index, hunk -> val hunksSeparation = if (index == 0) 0.dp else @@ -96,9 +97,9 @@ fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView: backgroundButton = color, onClick = { if (diffEntryType is DiffEntryType.StagedDiff) { - gitManager.unstageHunk(diffEntryType.diffEntry, hunk) + diffViewModel.unstageHunk(diffEntryType.diffEntry, hunk) } else { - gitManager.stageHunk(diffEntryType.diffEntry, hunk) + diffViewModel.stageHunk(diffEntryType.diffEntry, hunk) } } ) diff --git a/src/main/kotlin/app/ui/Remotes.kt b/src/main/kotlin/app/ui/Remotes.kt index 708f784..43f9ea7 100644 --- a/src/main/kotlin/app/ui/Remotes.kt +++ b/src/main/kotlin/app/ui/Remotes.kt @@ -12,16 +12,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.MAX_SIDE_PANEL_ITEMS_HEIGHT import app.extensions.simpleVisibleName -import app.git.GitManager +import app.git.TabViewModel import app.git.RemoteInfo import app.ui.components.ScrollableLazyColumn import app.ui.components.SideMenuEntry import app.ui.components.SideMenuSubentry import app.ui.components.entryHeight +import app.viewmodels.RemotesViewModel @Composable -fun Remotes(gitManager: GitManager) { - val remotes by gitManager.remotes.collectAsState() +fun Remotes(remotesViewModel: RemotesViewModel) { + val remotes by remotesViewModel.remotes.collectAsState() Column { SideMenuEntry("Remotes") diff --git a/src/main/kotlin/app/ui/RepositoryOpen.kt b/src/main/kotlin/app/ui/RepositoryOpen.kt index 3a0b3bc..8f72c5f 100644 --- a/src/main/kotlin/app/ui/RepositoryOpen.kt +++ b/src/main/kotlin/app/ui/RepositoryOpen.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.git.DiffEntryType -import app.git.GitManager +import app.git.TabViewModel import app.ui.dialogs.NewBranchDialog import app.ui.log.Log import openRepositoryDialog @@ -18,15 +18,22 @@ import org.jetbrains.compose.splitpane.rememberSplitPaneState @OptIn(ExperimentalSplitPaneApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class) @Composable -fun RepositoryOpenPage(gitManager: GitManager) { +fun RepositoryOpenPage(tabViewModel: TabViewModel) { + val repositoryState by tabViewModel.repositoryState.collectAsState() + var diffSelected by remember { mutableStateOf(null) } + LaunchedEffect(diffSelected) { + diffSelected?.let { safeDiffSelected -> + tabViewModel.updatedDiffEntry(safeDiffSelected) + } + } + var showNewBranchDialog by remember { mutableStateOf(false) } val (selectedItem, setSelectedItem) = remember { mutableStateOf(SelectedItem.None) } - LaunchedEffect(selectedItem) { diffSelected = null } @@ -37,7 +44,7 @@ fun RepositoryOpenPage(gitManager: GitManager) { showNewBranchDialog = false }, onAccept = { branchName -> - gitManager.createBranch(branchName) + tabViewModel.branchesViewModel.createBranch(branchName) showNewBranchDialog = false } ) @@ -46,12 +53,12 @@ fun RepositoryOpenPage(gitManager: GitManager) { Column { GMenu( onRepositoryOpen = { - openRepositoryDialog(gitManager = gitManager) + openRepositoryDialog(gitManager = tabViewModel) }, - onPull = { gitManager.pull() }, - onPush = { gitManager.push() }, - onStash = { gitManager.stash() }, - onPopStash = { gitManager.popStash() }, + onPull = { tabViewModel.pull() }, + onPush = { tabViewModel.push() }, + onStash = { tabViewModel.stash() }, + onPopStash = { tabViewModel.popStash() }, onCreateBranch = { showNewBranchDialog = true } ) @@ -65,22 +72,22 @@ fun RepositoryOpenPage(gitManager: GitManager) { .fillMaxHeight() ) { Branches( - gitManager = gitManager, + branchesViewModel = tabViewModel.branchesViewModel, onBranchClicked = { - val commit = gitManager.findCommit(it.objectId) + val commit = tabViewModel.findCommit(it.objectId) setSelectedItem(SelectedItem.Ref(commit)) } ) - Remotes(gitManager = gitManager) + Remotes(remotesViewModel = tabViewModel.remotesViewModel) Tags( - gitManager = gitManager, + tagsViewModel = tabViewModel.tagsViewModel, onTagClicked = { - val commit = gitManager.findCommit(it.objectId) + val commit = tabViewModel.findCommit(it.objectId) setSelectedItem(SelectedItem.Ref(commit)) } ) Stashes( - gitManager = gitManager, + gitManager = tabViewModel, onStashSelected = { stash -> setSelectedItem(SelectedItem.Stash(stash)) } @@ -97,11 +104,13 @@ fun RepositoryOpenPage(gitManager: GitManager) { modifier = Modifier .fillMaxSize() ) { - Crossfade(targetState = diffSelected) { diffEntry -> - when (diffEntry) { +// Crossfade(targetState = diffSelected) { diffEntry -> + when (diffSelected) { null -> { Log( - gitManager = gitManager, + tabViewModel = tabViewModel, + repositoryState = repositoryState, + logViewModel = tabViewModel.logViewModel, selectedItem = selectedItem, onItemSelected = { setSelectedItem(it) @@ -110,12 +119,11 @@ fun RepositoryOpenPage(gitManager: GitManager) { } else -> { Diff( - gitManager = gitManager, - diffEntryType = diffEntry, + diffViewModel = tabViewModel.diffViewModel, onCloseDiffView = { diffSelected = null }) } } - } +// } } } @@ -126,8 +134,9 @@ fun RepositoryOpenPage(gitManager: GitManager) { ) { if (selectedItem == SelectedItem.UncommitedChanges) { UncommitedChanges( - gitManager = gitManager, + statusViewModel = tabViewModel.statusViewModel, selectedEntryType = diffSelected, + repositoryState = repositoryState, onStagedDiffEntrySelected = { diffEntry -> diffSelected = if (diffEntry != null) DiffEntryType.StagedDiff(diffEntry) @@ -140,7 +149,7 @@ fun RepositoryOpenPage(gitManager: GitManager) { ) } else if (selectedItem is SelectedItem.CommitBasedItem) { CommitChanges( - gitManager = gitManager, + gitManager = tabViewModel, commit = selectedItem.revCommit, onDiffSelected = { diffEntry -> diffSelected = DiffEntryType.CommitDiff(diffEntry) diff --git a/src/main/kotlin/app/ui/Stashes.kt b/src/main/kotlin/app/ui/Stashes.kt index 3337bce..29cbef0 100644 --- a/src/main/kotlin/app/ui/Stashes.kt +++ b/src/main/kotlin/app/ui/Stashes.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import app.git.GitManager +import app.git.TabViewModel import app.git.StashStatus import app.ui.components.ScrollableLazyColumn import app.ui.components.SideMenuEntry @@ -15,7 +15,7 @@ import org.eclipse.jgit.revwalk.RevCommit @Composable fun Stashes( - gitManager: GitManager, + gitManager: TabViewModel, onStashSelected: (commit: RevCommit) -> Unit, ) { val stashStatusState = gitManager.stashStatus.collectAsState() diff --git a/src/main/kotlin/app/ui/SystemDialogs.kt b/src/main/kotlin/app/ui/SystemDialogs.kt index 6579030..1996cbc 100644 --- a/src/main/kotlin/app/ui/SystemDialogs.kt +++ b/src/main/kotlin/app/ui/SystemDialogs.kt @@ -1,9 +1,9 @@ import app.extensions.runCommand -import app.git.GitManager +import app.git.TabViewModel import javax.swing.JFileChooser -fun openRepositoryDialog(gitManager: GitManager) { +fun openRepositoryDialog(gitManager: TabViewModel) { val os = System.getProperty("os.name") val appStateManager = gitManager.appStateManager val latestDirectoryOpened = appStateManager.latestOpenedRepositoryPath @@ -29,7 +29,7 @@ fun openRepositoryDialog(gitManager: GitManager) { } private fun openRepositoryDialog( - gitManager: GitManager, + gitManager: TabViewModel, latestDirectoryOpened: String ) { diff --git a/src/main/kotlin/app/ui/Tags.kt b/src/main/kotlin/app/ui/Tags.kt index 4c8d4d5..05546f1 100644 --- a/src/main/kotlin/app/ui/Tags.kt +++ b/src/main/kotlin/app/ui/Tags.kt @@ -13,20 +13,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.MAX_SIDE_PANEL_ITEMS_HEIGHT import app.extensions.simpleName -import app.git.GitManager +import app.git.TabViewModel import app.ui.components.ScrollableLazyColumn import app.ui.components.SideMenuEntry import app.ui.components.SideMenuSubentry import app.ui.components.entryHeight import app.ui.context_menu.tagContextMenuItems +import app.viewmodels.TagsViewModel import org.eclipse.jgit.lib.Ref @Composable fun Tags( - gitManager: GitManager, + tagsViewModel: TagsViewModel, onTagClicked: (Ref) -> Unit, ) { - val tagsState = gitManager.tags.collectAsState() + val tagsState = tagsViewModel.tags.collectAsState() val tags = tagsState.value Column { @@ -46,8 +47,8 @@ fun Tags( TagRow( tag = tag, onTagClicked = { onTagClicked(tag) }, - onCheckoutTag = { gitManager.checkoutRef(tag) }, - onDeleteTag = { gitManager.deleteTag(tag) } + onCheckoutTag = { tagsViewModel.checkoutRef(tag) }, + onDeleteTag = { tagsViewModel.deleteTag(tag) } ) } } diff --git a/src/main/kotlin/app/ui/UncommitedChanges.kt b/src/main/kotlin/app/ui/UncommitedChanges.kt index dc21225..a500476 100644 --- a/src/main/kotlin/app/ui/UncommitedChanges.kt +++ b/src/main/kotlin/app/ui/UncommitedChanges.kt @@ -28,36 +28,30 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.extensions.filePath -import app.extensions.icon -import app.extensions.iconColor import app.extensions.isMerging import app.git.DiffEntryType -import app.git.GitManager -import app.git.StageStatus import app.git.StatusEntry import app.theme.headerBackground import app.theme.headerText import app.theme.primaryTextColor import app.ui.components.ScrollableLazyColumn import app.ui.components.SecondaryButton +import app.viewmodels.StageStatus +import app.viewmodels.StatusViewModel import org.eclipse.jgit.diff.DiffEntry +import org.eclipse.jgit.lib.RepositoryState @OptIn(ExperimentalAnimationApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class) @Composable fun UncommitedChanges( - gitManager: GitManager, + statusViewModel: StatusViewModel, selectedEntryType: DiffEntryType?, + repositoryState: RepositoryState, onStagedDiffEntrySelected: (DiffEntry?) -> Unit, onUnstagedDiffEntrySelected: (DiffEntry) -> Unit, ) { - val stageStatusState = gitManager.stageStatus.collectAsState() + val stageStatusState = statusViewModel.stageStatus.collectAsState() val stageStatus = stageStatusState.value - val lastCheck by gitManager.lastTimeChecked.collectAsState() - val repositoryState by gitManager.repositoryState.collectAsState() - - LaunchedEffect(lastCheck) { - gitManager.loadStatus() - } val staged: List val unstaged: List @@ -83,7 +77,7 @@ fun UncommitedChanges( var commitMessage by remember { mutableStateOf("") } val doCommit = { - gitManager.commit(commitMessage) + statusViewModel.commit(commitMessage) onStagedDiffEntrySelected(null) commitMessage = "" } @@ -111,13 +105,13 @@ fun UncommitedChanges( diffEntries = staged, onDiffEntrySelected = onStagedDiffEntrySelected, onDiffEntryOptionSelected = { - gitManager.unstage(it) + statusViewModel.unstage(it) }, onReset = { diffEntry -> - gitManager.resetStaged(diffEntry) + statusViewModel.resetStaged(diffEntry) }, onAllAction = { - gitManager.unstageAll() + statusViewModel.unstageAll() } ) @@ -132,13 +126,13 @@ fun UncommitedChanges( diffEntries = unstaged, onDiffEntrySelected = onUnstagedDiffEntrySelected, onDiffEntryOptionSelected = { - gitManager.stage(it) + statusViewModel.stage(it) }, onReset = { diffEntry -> - gitManager.resetUnstaged(diffEntry) + statusViewModel.resetUnstaged(diffEntry) }, { - gitManager.stageAll() + statusViewModel.stageAll() }, allActionTitle = "Stage all" ) diff --git a/src/main/kotlin/app/ui/WelcomePage.kt b/src/main/kotlin/app/ui/WelcomePage.kt index 501cc9d..781f53f 100644 --- a/src/main/kotlin/app/ui/WelcomePage.kt +++ b/src/main/kotlin/app/ui/WelcomePage.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.extensions.dirName import app.extensions.dirPath -import app.git.GitManager +import app.git.TabViewModel import app.theme.primaryTextColor import app.theme.secondaryTextColor import app.ui.dialogs.CloneDialog @@ -33,7 +33,7 @@ import java.net.URI @OptIn(ExperimentalMaterialApi::class) @Composable fun WelcomePage( - gitManager: GitManager, + gitManager: TabViewModel, ) { val appStateManager = gitManager.appStateManager var showCloneView by remember { mutableStateOf(false) } diff --git a/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt b/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt index 377d3fb..0b8c213 100644 --- a/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt +++ b/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt @@ -20,8 +20,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import app.AppStateManager +import app.di.AppComponent +import app.di.DaggerTabComponent +import app.git.TabViewModel import app.theme.tabColorActive import app.theme.tabColorInactive +import app.ui.AppTab +import javax.inject.Inject @Composable @@ -48,7 +54,7 @@ fun RepositoriesTabPanel( ) { items(items = tabs) { tab -> Tab( - title = tab.title, + title = tab.tabName, selected = tab.key == selectedTabKey, onClick = { onTabSelected(tab.key) @@ -154,7 +160,33 @@ fun Tab(title: MutableState, selected: Boolean, onClick: () -> Unit, onC } class TabInformation( - val title: MutableState, + val tabName: MutableState, val key: Int, + val path: String?, + appComponent: AppComponent, +) { + @Inject + lateinit var gitManager: TabViewModel + + @Inject + lateinit var appStateManager: AppStateManager + val content: @Composable (TabInformation) -> Unit -) \ No newline at end of file + + init { + val tabComponent = DaggerTabComponent.builder() + .appComponent(appComponent) + .build() + tabComponent.inject(this) + + gitManager.onRepositoryChanged = { path -> + if (path == null) { + appStateManager.repositoryTabRemoved(key) + } else + appStateManager.repositoryTabChanged(key, path) + } + content = { + AppTab(gitManager, path, tabName) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/ui/dialogs/CloneDialog.kt b/src/main/kotlin/app/ui/dialogs/CloneDialog.kt index 5a50411..22c4140 100644 --- a/src/main/kotlin/app/ui/dialogs/CloneDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/CloneDialog.kt @@ -12,13 +12,13 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.git.CloneStatus -import app.git.GitManager +import app.git.TabViewModel import app.theme.primaryTextColor import java.io.File @Composable fun CloneDialog( - gitManager: GitManager, + gitManager: TabViewModel, onClose: () -> Unit ) { val cloneStatus = gitManager.cloneStatus.collectAsState() diff --git a/src/main/kotlin/app/ui/log/Log.kt b/src/main/kotlin/app/ui/log/Log.kt index 7f9425e..2f6abbb 100644 --- a/src/main/kotlin/app/ui/log/Log.kt +++ b/src/main/kotlin/app/ui/log/Log.kt @@ -34,8 +34,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.extensions.* -import app.git.GitManager -import app.git.LogStatus +import app.git.TabViewModel import app.git.graph.GraphNode import app.theme.* import app.ui.SelectedItem @@ -47,6 +46,8 @@ import app.ui.dialogs.MergeDialog import app.ui.dialogs.NewBranchDialog import app.ui.dialogs.NewTagDialog import app.ui.dialogs.ResetBranchDialog +import app.viewmodels.LogStatus +import app.viewmodels.LogViewModel import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.revwalk.RevCommit @@ -70,13 +71,14 @@ private const val CANVAS_MIN_WIDTH = 100 ) @Composable fun Log( - gitManager: GitManager, + tabViewModel: TabViewModel, + logViewModel: LogViewModel, selectedItem: SelectedItem, onItemSelected: (SelectedItem) -> Unit, + repositoryState: RepositoryState, ) { - val logStatusState = gitManager.logStatus.collectAsState() + val logStatusState = logViewModel.logStatus.collectAsState() val logStatus = logStatusState.value - val repositoryState by gitManager.repositoryState.collectAsState() val showLogDialog = remember { mutableStateOf(LogDialog.None) } val selectedCommit = if (selectedItem is SelectedItem.CommitBasedItem) { @@ -86,6 +88,7 @@ fun Log( } if (logStatus is LogStatus.Loaded) { + val hasUncommitedChanges = logStatus.hasUncommitedChanges val commitList = logStatus.plotCommitList val scrollState = rememberLazyListState() @@ -102,7 +105,7 @@ fun Log( } LogDialogs( - gitManager, + logViewModel, currentBranch = logStatus.currentBranch, onResetShowLogDialog = { showLogDialog.value = LogDialog.None }, showLogDialog = showLogDialog.value, @@ -114,7 +117,7 @@ fun Log( .background(MaterialTheme.colors.background) .fillMaxSize() ) { - val hasUncommitedChanges by gitManager.hasUncommitedChanges.collectAsState() +// val hasUncommitedChanges by tabViewModel.hasUncommitedChanges.collectAsState() val weightMod = remember { mutableStateOf(0f) } var graphWidth = (CANVAS_MIN_WIDTH + weightMod.value).dp @@ -131,11 +134,12 @@ fun Log( .background(MaterialTheme.colors.background) .fillMaxSize(), ) { + //TODO: Shouldn't this be an item of the graph? if (hasUncommitedChanges) item { UncommitedChangesLine( selected = selectedItem == SelectedItem.UncommitedChanges, - hasPreviousCommits = commitList.count() > 0, + hasPreviousCommits = commitList.isNotEmpty(), graphWidth = graphWidth, weightMod = weightMod, repositoryState = repositoryState, @@ -146,7 +150,7 @@ fun Log( } items(items = commitList) { graphNode -> CommitLine( - gitManager = gitManager, + logViewModel = logViewModel, graphNode = graphNode, selected = selectedCommit?.name == graphNode.name, weightMod = weightMod, @@ -169,7 +173,7 @@ fun Log( @Composable fun LogDialogs( - gitManager: GitManager, + logViewModel: LogViewModel, onResetShowLogDialog: () -> Unit, showLogDialog: LogDialog, currentBranch: Ref?, @@ -179,7 +183,7 @@ fun LogDialogs( NewBranchDialog( onReject = onResetShowLogDialog, onAccept = { branchName -> - gitManager.createBranchOnCommit(branchName, showLogDialog.graphNode) + logViewModel.createBranchOnCommit(branchName, showLogDialog.graphNode) onResetShowLogDialog() } ) @@ -188,7 +192,7 @@ fun LogDialogs( NewTagDialog( onReject = onResetShowLogDialog, onAccept = { tagName -> - gitManager.createTagOnCommit(tagName, showLogDialog.graphNode) + logViewModel.createTagOnCommit(tagName, showLogDialog.graphNode) onResetShowLogDialog() } ) @@ -200,7 +204,7 @@ fun LogDialogs( mergeBranchName = showLogDialog.ref.simpleName, onReject = onResetShowLogDialog, onAccept = { ff -> - gitManager.mergeBranch(showLogDialog.ref, ff) + logViewModel.mergeBranch(showLogDialog.ref, ff) onResetShowLogDialog() } ) @@ -208,7 +212,7 @@ fun LogDialogs( is LogDialog.ResetBranch -> ResetBranchDialog( onReject = onResetShowLogDialog, onAccept = { resetType -> - gitManager.resetToCommit(showLogDialog.graphNode, resetType) + logViewModel.resetToCommit(showLogDialog.graphNode, resetType) onResetShowLogDialog() } ) @@ -324,7 +328,7 @@ fun UncommitedChangesLine( @Composable fun CommitLine( - gitManager: GitManager, + logViewModel: LogViewModel, graphNode: GraphNode, selected: Boolean, weightMod: MutableState, @@ -348,9 +352,7 @@ fun CommitLine( listOf( ContextMenuItem( label = "Checkout commit", - onClick = { - gitManager.checkoutCommit(graphNode) - }), + onClick = { logViewModel.checkoutCommit(graphNode) }), ContextMenuItem( label = "Create branch", onClick = showCreateNewBranch @@ -361,7 +363,7 @@ fun CommitLine( ), ContextMenuItem( label = "Revert commit", - onClick = { gitManager.revertCommit(graphNode) } + onClick = { logViewModel.revertCommit(graphNode) } ), ContextMenuItem( @@ -403,10 +405,10 @@ fun CommitLine( refs = commitRefs, nodeColor = nodeColor, currentBranch = currentBranch, - onCheckoutRef = { ref -> gitManager.checkoutRef(ref) }, + onCheckoutRef = { ref -> logViewModel.checkoutRef(ref) }, onMergeBranch = { ref -> onMergeBranch(ref) }, - onDeleteBranch = { ref -> gitManager.deleteBranch(ref) }, - onDeleteTag = { ref -> gitManager.deleteTag(ref) }, + onDeleteBranch = { ref -> logViewModel.deleteBranch(ref) }, + onDeleteTag = { ref -> logViewModel.deleteTag(ref) }, ) } } diff --git a/src/main/kotlin/app/viewmodels/BranchesViewModel.kt b/src/main/kotlin/app/viewmodels/BranchesViewModel.kt new file mode 100644 index 0000000..6ab8b6d --- /dev/null +++ b/src/main/kotlin/app/viewmodels/BranchesViewModel.kt @@ -0,0 +1,60 @@ +package app.viewmodels + +import app.git.BranchesManager +import app.git.RefreshType +import app.git.TabState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Ref +import org.eclipse.jgit.revwalk.RevCommit +import javax.inject.Inject + +class BranchesViewModel @Inject constructor( + private val branchesManager: BranchesManager, + private val tabState: TabState, +) { + private val _branches = MutableStateFlow>(listOf()) + val branches: StateFlow> + get() = _branches + + private val _currentBranch = MutableStateFlow("") + val currentBranch: StateFlow + get() = _currentBranch + + suspend fun loadBranches(git: Git) { + val branchesList = branchesManager.getBranches(git) + + _branches.value = branchesList + _currentBranch.value = branchesManager.currentBranchRef(git)?.name ?: "" + } + + fun createBranch(branchName: String) = tabState.safeProcessing { git -> + branchesManager.createBranch(git, branchName) + this.loadBranches(git) + + return@safeProcessing RefreshType.NONE + } + + fun mergeBranch(ref: Ref, fastForward: Boolean) = tabState.safeProcessing { git -> + branchesManager.mergeBranch(git, ref, fastForward) + + return@safeProcessing RefreshType.ALL_DATA + } + + fun deleteBranch(branch: Ref) =tabState.safeProcessing { git -> + branchesManager.deleteBranch(git, branch) + + return@safeProcessing RefreshType.ALL_DATA + } + + fun checkoutRef(ref: Ref) = tabState.safeProcessing { git -> + branchesManager.checkoutRef(git, ref) + + return@safeProcessing RefreshType.ALL_DATA + } + + suspend fun refresh(git: Git) { + loadBranches(git) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/DiffViewModel.kt b/src/main/kotlin/app/viewmodels/DiffViewModel.kt new file mode 100644 index 0000000..8dae798 --- /dev/null +++ b/src/main/kotlin/app/viewmodels/DiffViewModel.kt @@ -0,0 +1,44 @@ +package app.viewmodels + +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 + +class DiffViewModel @Inject constructor( + private val tabState: TabState, + private val diffManager: DiffManager, + private val statusManager: StatusManager, +) { + // TODO Maybe use a sealed class instead of a null to represent that a diff is not selected? + private val _diffResult = MutableStateFlow(null) + val diffResult: StateFlow = _diffResult + + suspend fun updateDiff(git: Git, diffEntryType: DiffEntryType) = withContext(Dispatchers.IO) { + _diffResult.value = null + + val hunks = diffManager.diffFormat(git, diffEntryType) + + _diffResult.value = DiffResult(diffEntryType, hunks) + } + + fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation { git -> + statusManager.stageHunk(git, diffEntry, hunk) + + return@runOperation RefreshType.UNCOMMITED_CHANGES + } + + fun unstageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation { git -> + statusManager.unstageHunk(git, diffEntry, hunk) + + return@runOperation RefreshType.UNCOMMITED_CHANGES + } +} + +data class DiffResult(val diffEntryType: DiffEntryType, val hunks: List) \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/LogViewModel.kt b/src/main/kotlin/app/viewmodels/LogViewModel.kt new file mode 100644 index 0000000..24c26f1 --- /dev/null +++ b/src/main/kotlin/app/viewmodels/LogViewModel.kt @@ -0,0 +1,97 @@ +package app.viewmodels + +import app.git.* +import app.git.graph.GraphCommitList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Ref +import org.eclipse.jgit.revwalk.RevCommit +import javax.inject.Inject + +class LogViewModel @Inject constructor( + private val logManager: LogManager, + private val statusManager: StatusManager, + private val branchesManager: BranchesManager, + private val tagsManager: TagsManager, + private val tabState: TabState, +) { + private val _logStatus = MutableStateFlow(LogStatus.Loading) + + val logStatus: StateFlow + get() = _logStatus + + suspend fun loadLog(git: Git) { + _logStatus.value = LogStatus.Loading + + val currentBranch = branchesManager.currentBranchRef(git) + val log = logManager.loadLog(git, currentBranch) + val hasUncommitedChanges = statusManager.hasUncommitedChanges(git) + _logStatus.value = LogStatus.Loaded(hasUncommitedChanges, log, currentBranch) + } + + fun checkoutCommit(revCommit: RevCommit) = tabState.safeProcessing { git -> + logManager.checkoutCommit(git, revCommit) + + return@safeProcessing RefreshType.ALL_DATA + } + + fun revertCommit(revCommit: RevCommit) = tabState.safeProcessing { git -> + logManager.revertCommit(git, revCommit) + + return@safeProcessing RefreshType.ALL_DATA + } + + fun resetToCommit(revCommit: RevCommit, resetType: ResetType) = tabState.safeProcessing { git -> + logManager.resetToCommit(git, revCommit, resetType = resetType) + + return@safeProcessing RefreshType.ALL_DATA + } + + fun checkoutRef(ref: Ref) = tabState.safeProcessing { git -> + branchesManager.checkoutRef(git, ref) + + return@safeProcessing RefreshType.ALL_DATA + } + + + fun createBranchOnCommit(branch: String, revCommit: RevCommit) = tabState.safeProcessing { git -> + branchesManager.createBranchOnCommit(git, branch, revCommit) + + return@safeProcessing RefreshType.ALL_DATA + } + + fun createTagOnCommit(tag: String, revCommit: RevCommit) = tabState.safeProcessing { git -> + tagsManager.createTagOnCommit(git, tag, revCommit) + + return@safeProcessing RefreshType.ALL_DATA + } + + fun mergeBranch(ref: Ref, fastForward: Boolean) = tabState.safeProcessing { git -> + branchesManager.mergeBranch(git, ref, fastForward) + + return@safeProcessing RefreshType.ALL_DATA + } + + fun deleteBranch(branch: Ref) =tabState.safeProcessing { git -> + branchesManager.deleteBranch(git, branch) + + return@safeProcessing RefreshType.ALL_DATA + } + + fun deleteTag(tag: Ref) = tabState.safeProcessing { git -> + tagsManager.deleteTag(git, tag) + + return@safeProcessing RefreshType.ALL_DATA + } + + suspend fun refresh(git: Git) { + loadLog(git) + } +} + +sealed class LogStatus { + object Loading : LogStatus() + class Loaded(val hasUncommitedChanges: Boolean, val plotCommitList: GraphCommitList, val currentBranch: Ref?) : LogStatus() +} \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/RemotesViewModel.kt b/src/main/kotlin/app/viewmodels/RemotesViewModel.kt new file mode 100644 index 0000000..eabaa21 --- /dev/null +++ b/src/main/kotlin/app/viewmodels/RemotesViewModel.kt @@ -0,0 +1,44 @@ +package app.viewmodels + +import app.git.BranchesManager +import app.git.RemoteInfo +import app.git.RemotesManager +import app.git.TabState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Ref +import org.eclipse.jgit.transport.RemoteConfig +import javax.inject.Inject + +class RemotesViewModel @Inject constructor( + private val remotesManager: RemotesManager, + private val branchesManager: BranchesManager, +) { + private val _remotes = MutableStateFlow>(listOf()) + val remotes: StateFlow> + get() = _remotes + + suspend fun loadRemotes(git: Git) = withContext(Dispatchers.IO) { + val remotes = git.remoteList() + .call() + val allRemoteBranches = branchesManager.remoteBranches(git) + + remotesManager.loadRemotes(git, allRemoteBranches) + val remoteInfoList = remotes.map { remoteConfig -> + val remoteBranches = allRemoteBranches.filter { branch -> + branch.name.startsWith("refs/remotes/${remoteConfig.name}") + } + RemoteInfo(remoteConfig, remoteBranches) + } + + _remotes.value = remoteInfoList + } + + suspend fun refresh(git: Git) = withContext(Dispatchers.IO) { + loadRemotes(git) + } +} + diff --git a/src/main/kotlin/app/viewmodels/StatusViewModel.kt b/src/main/kotlin/app/viewmodels/StatusViewModel.kt new file mode 100644 index 0000000..81df400 --- /dev/null +++ b/src/main/kotlin/app/viewmodels/StatusViewModel.kt @@ -0,0 +1,124 @@ +package app.viewmodels + +import app.git.* +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 + +class StatusViewModel @Inject constructor( + private val tabState: TabState, + private val statusManager: StatusManager, + private val branchesManager: BranchesManager, + private val repositoryManager: RepositoryManager, +) { + private val _stageStatus = MutableStateFlow(StageStatus.Loaded(listOf(), listOf())) + val stageStatus: StateFlow = _stageStatus + + + private val _hasUncommitedChanges = MutableStateFlow(false) + val hasUncommitedChanges: StateFlow + get() = _hasUncommitedChanges + + fun stage(diffEntry: DiffEntry) = tabState.runOperation { git -> + statusManager.stage(git, diffEntry) + loadStatus(git) + + return@runOperation RefreshType.NONE + } + + fun unstage(diffEntry: DiffEntry) = tabState.runOperation { git -> + statusManager.unstage(git, diffEntry) + loadStatus(git) + + return@runOperation RefreshType.NONE + } + + + + fun unstageAll() = tabState.safeProcessing { git -> + statusManager.unstageAll(git) + loadStatus(git) + + return@safeProcessing RefreshType.NONE + } + + fun stageAll() = tabState.safeProcessing { git -> + statusManager.stageAll(git) + loadStatus(git) + + return@safeProcessing RefreshType.NONE + } + + + + fun resetStaged(diffEntry: DiffEntry) = tabState.runOperation { git -> + statusManager.reset(git, diffEntry, staged = true) + + return@runOperation RefreshType.UNCOMMITED_CHANGES + } + + fun resetUnstaged(diffEntry: DiffEntry) =tabState.runOperation { git -> + statusManager.reset(git, diffEntry, staged = false) + + return@runOperation RefreshType.UNCOMMITED_CHANGES + } + + suspend fun loadStatus(git: Git) { + val previousStatus = _stageStatus.value + + try { + _stageStatus.value = StageStatus.Loading + val repositoryState = repositoryManager.getRepositoryState(git) + val currentBranchRef = branchesManager.currentBranchRef(git) + val staged = statusManager.getStaged(git, currentBranchRef, repositoryState) + val unstaged = statusManager.getUnstaged(git, repositoryState) + + _stageStatus.value = StageStatus.Loaded(staged, unstaged) + } catch (ex: Exception) { + _stageStatus.value = previousStatus + throw ex + } + } + + suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) { + _hasUncommitedChanges.value = statusManager.hasUncommitedChanges(git) + } + + fun commit(message: String) = tabState.safeProcessing { git -> + statusManager.commit(git, message) + + return@safeProcessing RefreshType.ALL_DATA + } + + + + suspend fun refresh(git: Git) = withContext(Dispatchers.IO) { + loadStatus(git) + loadHasUncommitedChanges(git) + } + + /** + * Checks if there are uncommited changes and returns if the state has changed ( + */ + suspend fun updateHasUncommitedChanges(git: Git): Boolean { + val hadUncommitedChanges = hasUncommitedChanges.value + + loadStatus(git) + + val hasNowUncommitedChanges = hasUncommitedChanges.value + + // Return true to update the log only if the uncommitedChanges status has changed + return (hasNowUncommitedChanges != hadUncommitedChanges) + } +} + +sealed class StageStatus { + object Loading : StageStatus() + data class Loaded(val staged: List, val unstaged: List) : StageStatus() +} + diff --git a/src/main/kotlin/app/git/GitManager.kt b/src/main/kotlin/app/viewmodels/TabViewModel.kt similarity index 53% rename from src/main/kotlin/app/git/GitManager.kt rename to src/main/kotlin/app/viewmodels/TabViewModel.kt index e2f46d3..9105b54 100644 --- a/src/main/kotlin/app/git/GitManager.kt +++ b/src/main/kotlin/app/viewmodels/TabViewModel.kt @@ -6,6 +6,7 @@ import app.app.newErrorNow import app.credentials.CredentialsState import app.credentials.CredentialsStateManager import app.git.diff.Hunk +import app.viewmodels.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -13,7 +14,6 @@ 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.Ref import org.eclipse.jgit.lib.Repository import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.revwalk.RevCommit @@ -22,15 +22,18 @@ import java.io.File import javax.inject.Inject -class GitManager @Inject constructor( - private val statusManager: StatusManager, - private val logManager: LogManager, +class TabViewModel @Inject constructor( + val logViewModel: LogViewModel, + val branchesViewModel: BranchesViewModel, + val tagsViewModel: TagsViewModel, + val remotesViewModel: RemotesViewModel, + val statusViewModel: StatusViewModel, + val diffViewModel: DiffViewModel, + private val repositoryManager: RepositoryManager, private val remoteOperationsManager: RemoteOperationsManager, - private val branchesManager: BranchesManager, private val stashManager: StashManager, private val diffManager: DiffManager, - private val tagsManager: TagsManager, - private val remotesManager: RemotesManager, + private val tabState: TabState, val errorsManager: ErrorsManager, val appStateManager: AppStateManager, private val fileChangesWatcher: FileChangesWatcher, @@ -50,22 +53,31 @@ class GitManager @Inject constructor( val processing: StateFlow get() = _processing - private val _lastTimeChecked = MutableStateFlow(System.currentTimeMillis()) - val lastTimeChecked: StateFlow - get() = _lastTimeChecked - - val stageStatus: StateFlow = statusManager.stageStatus - val repositoryState: StateFlow = statusManager.repositoryState - val logStatus: StateFlow = logManager.logStatus - val branches: StateFlow> = branchesManager.branches - val tags: StateFlow> = tagsManager.tags - val currentBranch: StateFlow = branchesManager.currentBranch val stashStatus: StateFlow = stashManager.stashStatus val credentialsState: StateFlow = credentialsStateManager.credentialsState val cloneStatus: StateFlow = remoteOperationsManager.cloneStatus - val remotes: StateFlow> = remotesManager.remotes - private var git: Git? = null + private val _repositoryState = MutableStateFlow(RepositoryState.SAFE) + val repositoryState: StateFlow = _repositoryState + + init { + 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() + } + } + } + } + + private fun refreshLog() = tabState.runOperation { git -> + logViewModel.refresh(git) + + return@runOperation RefreshType.NONE + } /** * Property that indicates if a git operation is running @@ -74,7 +86,7 @@ class GitManager @Inject constructor( private val safeGit: Git get() { - val git = this.git + val git = this.tabState.git if (git == null) { _repositorySelectionStatus.value = RepositorySelectionStatus.None throw CancellationException() @@ -110,13 +122,15 @@ class GitManager @Inject constructor( try { repository.workTree // test if repository is valid _repositorySelectionStatus.value = RepositorySelectionStatus.Open(repository) - git = Git(repository) + tabState.git = Git(repository) onRepositoryChanged(repository.directory.parent) refreshRepositoryInfo() launch { watchRepositoryChanges() } + + println("AppStateManagerReference $appStateManager") } catch (ex: Exception) { ex.printStackTrace() onRepositoryChanged(null) @@ -125,6 +139,10 @@ class GitManager @Inject constructor( } } + suspend fun loadRepositoryState(git: Git) = withContext(Dispatchers.IO) { + _repositoryState.value = repositoryManager.getRepositoryState(git) + } + private suspend fun watchRepositoryChanges() { val ignored = safeGit.status().call().ignoredNotInIndex.toList() @@ -135,78 +153,35 @@ class GitManager @Inject constructor( if (!operationRunning) { // Only update if there isn't any process running safeProcessing(showError = false) { println("Changes detected, loading status") - val hasUncommitedChanges = statusManager.hasUncommitedChanges.value - statusManager.loadHasUncommitedChanges(safeGit) - statusManager.loadStatus(safeGit) +// val hasUncommitedChanges = statusManager.hasUncommitedChanges.value +// statusManager.loadHasUncommitedChanges(safeGit) +// statusManager.loadStatus(safeGit) - if(!hasUncommitedChanges) { - logManager.loadLog(safeGit) - } + statusViewModel.refresh(safeGit) + checkUncommitedChanges() } } } } - fun loadLog() = managerScope.launch { - coLoadLog() + private suspend fun loadLog() { + logViewModel.loadLog(safeGit) } - private suspend fun coLoadLog() { - logManager.loadLog(safeGit) - } - - suspend fun loadStatus() { - val hadUncommitedChanges = statusManager.hasUncommitedChanges.value - - statusManager.loadStatus(safeGit) - - val hasNowUncommitedChanges = statusManager.hasUncommitedChanges.value + suspend fun checkUncommitedChanges() { + val uncommitedChangesStateChanged = statusViewModel.updateHasUncommitedChanges(safeGit) // Update the log only if the uncommitedChanges status has changed - if (hasNowUncommitedChanges != hadUncommitedChanges) - coLoadLog() + if (uncommitedChangesStateChanged) + loadLog() } - fun stage(diffEntry: DiffEntry) = managerScope.launch { - runOperation { - statusManager.stage(safeGit, diffEntry) - } - } - - fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = managerScope.launch { - runOperation { - statusManager.stageHunk(safeGit, diffEntry, hunk) - } - } - - fun unstageHunk(diffEntry: DiffEntry, hunk: Hunk) = managerScope.launch { - runOperation { - statusManager.unstageHunk(safeGit, diffEntry, hunk) - } - } - - fun unstage(diffEntry: DiffEntry) = managerScope.launch { - runOperation { - statusManager.unstage(safeGit, diffEntry) - } - } - - fun commit(message: String) = managerScope.launch { - safeProcessing { - statusManager.commit(safeGit, message) - refreshRepositoryInfo() - } - } - - val hasUncommitedChanges: StateFlow - get() = statusManager.hasUncommitedChanges - suspend fun diffFormat(diffEntryType: DiffEntryType): List { try { return diffManager.diffFormat(safeGit, diffEntryType) } catch (ex: Exception) { ex.printStackTrace() - loadStatus() + checkUncommitedChanges() return listOf() } } @@ -214,7 +189,7 @@ class GitManager @Inject constructor( fun pull() = managerScope.launch { safeProcessing { remoteOperationsManager.pull(safeGit) - coLoadLog() + loadLog() } } @@ -223,26 +198,27 @@ class GitManager @Inject constructor( try { remoteOperationsManager.push(safeGit) } finally { - coLoadLog() + loadLog() } } } private suspend fun refreshRepositoryInfo() { - statusManager.loadRepositoryStatus(safeGit) - statusManager.loadHasUncommitedChanges(safeGit) - statusManager.loadStatus(safeGit) - branchesManager.loadBranches(safeGit) - remotesManager.loadRemotes(safeGit, branchesManager.remoteBranches(safeGit)) - tagsManager.loadTags(safeGit) + logViewModel.refresh(safeGit) + branchesViewModel.refresh(safeGit) + remotesViewModel.refresh(safeGit) + tagsViewModel.refresh(safeGit) + statusViewModel.refresh(safeGit) + loadRepositoryState(safeGit) + stashManager.loadStashList(safeGit) - coLoadLog() + loadLog() } fun stash() = managerScope.launch { safeProcessing { stashManager.stash(safeGit) - loadStatus() + checkUncommitedChanges() loadLog() } } @@ -250,42 +226,11 @@ class GitManager @Inject constructor( fun popStash() = managerScope.launch { safeProcessing { stashManager.popStash(safeGit) - loadStatus() + checkUncommitedChanges() loadLog() } } - fun createBranch(branchName: String) = managerScope.launch { - safeProcessing { - branchesManager.createBranch(safeGit, branchName) - coLoadLog() - } - } - - fun deleteBranch(branch: Ref) = managerScope.launch { - safeProcessing { - branchesManager.deleteBranch(safeGit, branch) - refreshRepositoryInfo() - } - } - - fun deleteTag(tag: Ref) = managerScope.launch { - safeProcessing { - tagsManager.deleteTag(safeGit, tag) - refreshRepositoryInfo() - } - } - - fun resetStaged(diffEntry: DiffEntry) = managerScope.launch { - statusManager.reset(safeGit, diffEntry, staged = true) - loadLog() - } - - fun resetUnstaged(diffEntry: DiffEntry) = managerScope.launch { - statusManager.reset(safeGit, diffEntry, staged = false) - loadLog() - } - fun credentialsDenied() { credentialsStateManager.updateState(CredentialsState.CredentialsDenied) } @@ -302,68 +247,8 @@ class GitManager @Inject constructor( return diffManager.commitDiffEntries(safeGit, commit) } - fun unstageAll() = managerScope.launch { - safeProcessing { - statusManager.unstageAll(safeGit) - } - } - - fun stageAll() = managerScope.launch { - safeProcessing { - statusManager.stageAll(safeGit) - } - } - - fun checkoutCommit(revCommit: RevCommit) = managerScope.launch { - safeProcessing { - logManager.checkoutCommit(safeGit, revCommit) - refreshRepositoryInfo() - } - } - - fun revertCommit(revCommit: RevCommit) = managerScope.launch { - safeProcessing { - logManager.revertCommit(safeGit, revCommit) - refreshRepositoryInfo() - } - } - - fun resetToCommit(revCommit: RevCommit, resetType: ResetType) = managerScope.launch { - safeProcessing { - logManager.resetToCommit(safeGit, revCommit, resetType = resetType) - refreshRepositoryInfo() - } - } - - fun createBranchOnCommit(branch: String, revCommit: RevCommit) = managerScope.launch { - safeProcessing { - branchesManager.createBranchOnCommit(safeGit, branch, revCommit) - refreshRepositoryInfo() - } - } - - fun createTagOnCommit(tag: String, revCommit: RevCommit) = managerScope.launch { - safeProcessing { - tagsManager.createTagOnCommit(safeGit, tag, revCommit) - refreshRepositoryInfo() - } - } - var onRepositoryChanged: (path: String?) -> Unit = {} - fun checkoutRef(ref: Ref) = managerScope.launch { - safeProcessing { - logManager.checkoutRef(safeGit, ref) - refreshRepositoryInfo() - } - } - - fun mergeBranch(ref: Ref, fastForward: Boolean) = managerScope.launch { - safeProcessing { - branchesManager.mergeBranch(safeGit, ref, fastForward) - refreshRepositoryInfo() - } - } fun dispose() { managerScope.cancel() @@ -377,7 +262,6 @@ class GitManager @Inject constructor( return safeGit.repository.parseCommit(objectId) } - @Synchronized private suspend fun safeProcessing(showError: Boolean = true, callback: suspend () -> Unit) { _processing.value = true operationRunning = true @@ -395,13 +279,10 @@ class GitManager @Inject constructor( } } - private inline fun runOperation(block: () -> Unit) { - operationRunning = true - try { - block() - } finally { - operationRunning = false - } + fun updatedDiffEntry(diffSelected: DiffEntryType) = tabState.runOperation { git -> + diffViewModel.updateDiff(git , diffSelected) + + return@runOperation RefreshType.NONE } } @@ -410,4 +291,4 @@ sealed class RepositorySelectionStatus { object None : RepositorySelectionStatus() object Loading : RepositorySelectionStatus() data class Open(val repository: Repository) : RepositorySelectionStatus() -} \ No newline at end of file +} diff --git a/src/main/kotlin/app/viewmodels/TagsViewModel.kt b/src/main/kotlin/app/viewmodels/TagsViewModel.kt new file mode 100644 index 0000000..11abb3d --- /dev/null +++ b/src/main/kotlin/app/viewmodels/TagsViewModel.kt @@ -0,0 +1,45 @@ +package app.viewmodels + +import app.git.BranchesManager +import app.git.RefreshType +import app.git.TabState +import app.git.TagsManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Ref +import javax.inject.Inject + +class TagsViewModel @Inject constructor( + private val tabState: TabState, + private val branchesManager: BranchesManager, + private val tagsManager: TagsManager, +) { + private val _tags = MutableStateFlow>(listOf()) + val tags: StateFlow> + get() = _tags + + suspend fun loadTags(git: Git) = withContext(Dispatchers.IO) { + val tagsList = tagsManager.getTags(git) + + _tags.value = tagsList + } + + fun checkoutRef(ref: Ref) = tabState.safeProcessing { git -> + branchesManager.checkoutRef(git, ref) + + return@safeProcessing RefreshType.ALL_DATA + } + + fun deleteTag(tag: Ref) = tabState.safeProcessing { git -> + tagsManager.deleteTag(git, tag) + + return@safeProcessing RefreshType.ALL_DATA + } + + suspend fun refresh(git: Git) { + loadTags(git) + } +} \ No newline at end of file From e6619cbd4b9cd9ac4bdc61ffc738d1591dfb2710 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Mon, 3 Jan 2022 22:59:35 +0100 Subject: [PATCH 2/5] Diff scroll is now preserved when changing the diff entry The scroll is stored in the view model and only resetted when changing to a different file or state (staged/unstaged version of the same file). --- src/main/kotlin/app/git/BranchesManager.kt | 8 --- src/main/kotlin/app/ui/AppTab.kt | 4 +- src/main/kotlin/app/ui/Branches.kt | 1 - src/main/kotlin/app/ui/CommitChanges.kt | 2 +- src/main/kotlin/app/ui/Diff.kt | 7 +-- src/main/kotlin/app/ui/Remotes.kt | 1 - src/main/kotlin/app/ui/RepositoryOpen.kt | 58 ++++++++----------- src/main/kotlin/app/ui/Stashes.kt | 2 +- src/main/kotlin/app/ui/SystemDialogs.kt | 2 +- src/main/kotlin/app/ui/Tags.kt | 1 - src/main/kotlin/app/ui/UncommitedChanges.kt | 2 +- src/main/kotlin/app/ui/WelcomePage.kt | 2 +- .../app/ui/components/RepositoriesTabPanel.kt | 2 +- src/main/kotlin/app/ui/dialogs/CloneDialog.kt | 2 +- src/main/kotlin/app/ui/log/Log.kt | 2 +- .../kotlin/app/viewmodels/DiffViewModel.kt | 25 ++++++++ .../kotlin/app/viewmodels/StatusViewModel.kt | 12 ++-- .../kotlin/app/viewmodels/TabViewModel.kt | 40 +++++++------ 18 files changed, 87 insertions(+), 86 deletions(-) diff --git a/src/main/kotlin/app/git/BranchesManager.kt b/src/main/kotlin/app/git/BranchesManager.kt index 757b0f2..8af5ea4 100644 --- a/src/main/kotlin/app/git/BranchesManager.kt +++ b/src/main/kotlin/app/git/BranchesManager.kt @@ -15,14 +15,6 @@ import org.eclipse.jgit.revwalk.RevCommit import javax.inject.Inject class BranchesManager @Inject constructor() { - private val _branches = MutableStateFlow>(listOf()) - val branches: StateFlow> - get() = _branches - - private val _currentBranch = MutableStateFlow("") - val currentBranch: StateFlow - get() = _currentBranch - /** * Returns the current branch in [Ref]. If the repository is new, the current branch will be null. */ diff --git a/src/main/kotlin/app/ui/AppTab.kt b/src/main/kotlin/app/ui/AppTab.kt index b240b9a..d70b180 100644 --- a/src/main/kotlin/app/ui/AppTab.kt +++ b/src/main/kotlin/app/ui/AppTab.kt @@ -21,8 +21,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.LoadingRepository import app.credentials.CredentialsState -import app.git.TabViewModel -import app.git.RepositorySelectionStatus +import app.viewmodels.TabViewModel +import app.viewmodels.RepositorySelectionStatus import app.ui.dialogs.PasswordDialog import app.ui.dialogs.UserPasswordDialog import kotlinx.coroutines.delay diff --git a/src/main/kotlin/app/ui/Branches.kt b/src/main/kotlin/app/ui/Branches.kt index 5ff12d4..1962da0 100644 --- a/src/main/kotlin/app/ui/Branches.kt +++ b/src/main/kotlin/app/ui/Branches.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.unit.dp import app.MAX_SIDE_PANEL_ITEMS_HEIGHT import app.extensions.isLocal import app.extensions.simpleName -import app.git.TabViewModel import app.ui.components.ScrollableLazyColumn import app.ui.components.SideMenuEntry import app.ui.components.SideMenuSubentry diff --git a/src/main/kotlin/app/ui/CommitChanges.kt b/src/main/kotlin/app/ui/CommitChanges.kt index 193af5b..8145950 100644 --- a/src/main/kotlin/app/ui/CommitChanges.kt +++ b/src/main/kotlin/app/ui/CommitChanges.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.extensions.* -import app.git.TabViewModel +import app.viewmodels.TabViewModel import app.theme.headerBackground import app.theme.headerText import app.theme.primaryTextColor diff --git a/src/main/kotlin/app/ui/Diff.kt b/src/main/kotlin/app/ui/Diff.kt index f0786cd..667b745 100644 --- a/src/main/kotlin/app/ui/Diff.kt +++ b/src/main/kotlin/app/ui/Diff.kt @@ -16,8 +16,6 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.git.DiffEntryType -import app.git.TabViewModel -import app.git.diff.Hunk import app.git.diff.LineType import app.theme.primaryTextColor import app.ui.components.ScrollableLazyColumn @@ -55,10 +53,11 @@ fun Diff( Text("Close diff") } + val scrollState by diffViewModel.lazyListState.collectAsState() ScrollableLazyColumn( modifier = Modifier - .fillMaxSize() -// .padding(16.dp) + .fillMaxSize(), + state = scrollState ) { itemsIndexed(hunks) { index, hunk -> val hunksSeparation = if (index == 0) diff --git a/src/main/kotlin/app/ui/Remotes.kt b/src/main/kotlin/app/ui/Remotes.kt index 43f9ea7..f1f3160 100644 --- a/src/main/kotlin/app/ui/Remotes.kt +++ b/src/main/kotlin/app/ui/Remotes.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.MAX_SIDE_PANEL_ITEMS_HEIGHT import app.extensions.simpleVisibleName -import app.git.TabViewModel import app.git.RemoteInfo import app.ui.components.ScrollableLazyColumn import app.ui.components.SideMenuEntry diff --git a/src/main/kotlin/app/ui/RepositoryOpen.kt b/src/main/kotlin/app/ui/RepositoryOpen.kt index 8f72c5f..44a5d81 100644 --- a/src/main/kotlin/app/ui/RepositoryOpen.kt +++ b/src/main/kotlin/app/ui/RepositoryOpen.kt @@ -1,12 +1,11 @@ package app.ui -import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.git.DiffEntryType -import app.git.TabViewModel +import app.viewmodels.TabViewModel import app.ui.dialogs.NewBranchDialog import app.ui.log.Log import openRepositoryDialog @@ -20,22 +19,13 @@ import org.jetbrains.compose.splitpane.rememberSplitPaneState @Composable fun RepositoryOpenPage(tabViewModel: TabViewModel) { val repositoryState by tabViewModel.repositoryState.collectAsState() - - var diffSelected by remember { - mutableStateOf(null) - } - - LaunchedEffect(diffSelected) { - diffSelected?.let { safeDiffSelected -> - tabViewModel.updatedDiffEntry(safeDiffSelected) - } - } + val diffSelected by tabViewModel.diffSelected.collectAsState() var showNewBranchDialog by remember { mutableStateOf(false) } val (selectedItem, setSelectedItem) = remember { mutableStateOf(SelectedItem.None) } LaunchedEffect(selectedItem) { - diffSelected = null + tabViewModel.newDiffSelected = null } if (showNewBranchDialog) { @@ -104,26 +94,24 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { modifier = Modifier .fillMaxSize() ) { -// Crossfade(targetState = diffSelected) { diffEntry -> - when (diffSelected) { - null -> { - Log( - tabViewModel = tabViewModel, - repositoryState = repositoryState, - logViewModel = tabViewModel.logViewModel, - selectedItem = selectedItem, - onItemSelected = { - setSelectedItem(it) - }, - ) - } - else -> { - Diff( - diffViewModel = tabViewModel.diffViewModel, - onCloseDiffView = { diffSelected = null }) - } + when (diffSelected) { + null -> { + Log( + tabViewModel = tabViewModel, + repositoryState = repositoryState, + logViewModel = tabViewModel.logViewModel, + selectedItem = selectedItem, + onItemSelected = { + setSelectedItem(it) + }, + ) } -// } + else -> { + Diff( + diffViewModel = tabViewModel.diffViewModel, + onCloseDiffView = { tabViewModel.newDiffSelected = null }) + } + } } } @@ -138,13 +126,13 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { selectedEntryType = diffSelected, repositoryState = repositoryState, onStagedDiffEntrySelected = { diffEntry -> - diffSelected = if (diffEntry != null) + tabViewModel.newDiffSelected = if (diffEntry != null) DiffEntryType.StagedDiff(diffEntry) else null }, onUnstagedDiffEntrySelected = { diffEntry -> - diffSelected = DiffEntryType.UnstagedDiff(diffEntry) + tabViewModel.newDiffSelected = DiffEntryType.UnstagedDiff(diffEntry) } ) } else if (selectedItem is SelectedItem.CommitBasedItem) { @@ -152,7 +140,7 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { gitManager = tabViewModel, commit = selectedItem.revCommit, onDiffSelected = { diffEntry -> - diffSelected = DiffEntryType.CommitDiff(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 29cbef0..7308c1c 100644 --- a/src/main/kotlin/app/ui/Stashes.kt +++ b/src/main/kotlin/app/ui/Stashes.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import app.git.TabViewModel +import app.viewmodels.TabViewModel import app.git.StashStatus import app.ui.components.ScrollableLazyColumn import app.ui.components.SideMenuEntry diff --git a/src/main/kotlin/app/ui/SystemDialogs.kt b/src/main/kotlin/app/ui/SystemDialogs.kt index 1996cbc..9799b77 100644 --- a/src/main/kotlin/app/ui/SystemDialogs.kt +++ b/src/main/kotlin/app/ui/SystemDialogs.kt @@ -1,5 +1,5 @@ import app.extensions.runCommand -import app.git.TabViewModel +import app.viewmodels.TabViewModel import javax.swing.JFileChooser diff --git a/src/main/kotlin/app/ui/Tags.kt b/src/main/kotlin/app/ui/Tags.kt index 05546f1..e1f77d0 100644 --- a/src/main/kotlin/app/ui/Tags.kt +++ b/src/main/kotlin/app/ui/Tags.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.MAX_SIDE_PANEL_ITEMS_HEIGHT import app.extensions.simpleName -import app.git.TabViewModel import app.ui.components.ScrollableLazyColumn import app.ui.components.SideMenuEntry import app.ui.components.SideMenuSubentry diff --git a/src/main/kotlin/app/ui/UncommitedChanges.kt b/src/main/kotlin/app/ui/UncommitedChanges.kt index a500476..c639af7 100644 --- a/src/main/kotlin/app/ui/UncommitedChanges.kt +++ b/src/main/kotlin/app/ui/UncommitedChanges.kt @@ -74,7 +74,6 @@ fun UncommitedChanges( unstaged = listOf() // return empty lists if still loading } - var commitMessage by remember { mutableStateOf("") } val doCommit = { statusViewModel.commit(commitMessage) @@ -190,6 +189,7 @@ fun UncommitedChanges( } } +// TODO: This logic should be part of the diffViewModel where it gets the latest version of the diffEntry fun checkIfSelectedEntryShouldBeUpdated( selectedEntryType: DiffEntryType, staged: List, diff --git a/src/main/kotlin/app/ui/WelcomePage.kt b/src/main/kotlin/app/ui/WelcomePage.kt index 781f53f..69b5445 100644 --- a/src/main/kotlin/app/ui/WelcomePage.kt +++ b/src/main/kotlin/app/ui/WelcomePage.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.extensions.dirName import app.extensions.dirPath -import app.git.TabViewModel +import app.viewmodels.TabViewModel import app.theme.primaryTextColor import app.theme.secondaryTextColor import app.ui.dialogs.CloneDialog diff --git a/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt b/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt index 0b8c213..b9d5733 100644 --- a/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt +++ b/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp import app.AppStateManager import app.di.AppComponent import app.di.DaggerTabComponent -import app.git.TabViewModel +import app.viewmodels.TabViewModel import app.theme.tabColorActive import app.theme.tabColorInactive import app.ui.AppTab diff --git a/src/main/kotlin/app/ui/dialogs/CloneDialog.kt b/src/main/kotlin/app/ui/dialogs/CloneDialog.kt index 22c4140..53a7703 100644 --- a/src/main/kotlin/app/ui/dialogs/CloneDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/CloneDialog.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.git.CloneStatus -import app.git.TabViewModel +import app.viewmodels.TabViewModel import app.theme.primaryTextColor import java.io.File diff --git a/src/main/kotlin/app/ui/log/Log.kt b/src/main/kotlin/app/ui/log/Log.kt index 2f6abbb..b480a3c 100644 --- a/src/main/kotlin/app/ui/log/Log.kt +++ b/src/main/kotlin/app/ui/log/Log.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.extensions.* -import app.git.TabViewModel +import app.viewmodels.TabViewModel import app.git.graph.GraphNode import app.theme.* import app.ui.SelectedItem diff --git a/src/main/kotlin/app/viewmodels/DiffViewModel.kt b/src/main/kotlin/app/viewmodels/DiffViewModel.kt index 8dae798..7a3d868 100644 --- a/src/main/kotlin/app/viewmodels/DiffViewModel.kt +++ b/src/main/kotlin/app/viewmodels/DiffViewModel.kt @@ -1,5 +1,9 @@ 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 @@ -20,9 +24,30 @@ class DiffViewModel @Inject constructor( private val _diffResult = MutableStateFlow(null) val diffResult: StateFlow = _diffResult + val lazyListState = MutableStateFlow( + LazyListState( + 0, + 0 + ) + ) + suspend fun updateDiff(git: Git, diffEntryType: DiffEntryType) = withContext(Dispatchers.IO) { + val oldDiffEntryType = _diffResult.value?.diffEntryType + _diffResult.value = null + // If it's a different file or different state (index or workdir), reset the scroll state + if(oldDiffEntryType != null && + (oldDiffEntryType.diffEntry.oldPath != diffEntryType.diffEntry.oldPath || + oldDiffEntryType.diffEntry.newPath != diffEntryType.diffEntry.newPath || + oldDiffEntryType::class != diffEntryType::class) + ) { + lazyListState.value = LazyListState( + 0, + 0 + ) + } + val hunks = diffManager.diffFormat(git, diffEntryType) _diffResult.value = DiffResult(diffEntryType, hunks) diff --git a/src/main/kotlin/app/viewmodels/StatusViewModel.kt b/src/main/kotlin/app/viewmodels/StatusViewModel.kt index 81df400..6a2e394 100644 --- a/src/main/kotlin/app/viewmodels/StatusViewModel.kt +++ b/src/main/kotlin/app/viewmodels/StatusViewModel.kt @@ -26,32 +26,28 @@ class StatusViewModel @Inject constructor( fun stage(diffEntry: DiffEntry) = tabState.runOperation { git -> statusManager.stage(git, diffEntry) - loadStatus(git) - return@runOperation RefreshType.NONE + return@runOperation RefreshType.UNCOMMITED_CHANGES } fun unstage(diffEntry: DiffEntry) = tabState.runOperation { git -> statusManager.unstage(git, diffEntry) - loadStatus(git) - return@runOperation RefreshType.NONE + return@runOperation RefreshType.UNCOMMITED_CHANGES } fun unstageAll() = tabState.safeProcessing { git -> statusManager.unstageAll(git) - loadStatus(git) - return@safeProcessing RefreshType.NONE + return@safeProcessing RefreshType.UNCOMMITED_CHANGES } fun stageAll() = tabState.safeProcessing { git -> statusManager.stageAll(git) - loadStatus(git) - return@safeProcessing RefreshType.NONE + return@safeProcessing RefreshType.UNCOMMITED_CHANGES } diff --git a/src/main/kotlin/app/viewmodels/TabViewModel.kt b/src/main/kotlin/app/viewmodels/TabViewModel.kt index 9105b54..dec2bdf 100644 --- a/src/main/kotlin/app/viewmodels/TabViewModel.kt +++ b/src/main/kotlin/app/viewmodels/TabViewModel.kt @@ -1,12 +1,11 @@ -package app.git +package app.viewmodels import app.AppStateManager import app.app.ErrorsManager import app.app.newErrorNow import app.credentials.CredentialsState import app.credentials.CredentialsStateManager -import app.git.diff.Hunk -import app.viewmodels.* +import app.git.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -38,6 +37,7 @@ class TabViewModel @Inject constructor( val appStateManager: AppStateManager, private val fileChangesWatcher: FileChangesWatcher, ) { + val repositoryName: String get() = safeGit.repository.directory.parentFile.name @@ -57,6 +57,16 @@ class TabViewModel @Inject constructor( val credentialsState: StateFlow = credentialsStateManager.credentialsState val cloneStatus: StateFlow = remoteOperationsManager.cloneStatus + private val _diffSelected = MutableStateFlow(null) + val diffSelected : StateFlow = _diffSelected + var newDiffSelected: DiffEntryType? + get() = diffSelected.value + set(value){ + _diffSelected.value = value + + updateDiffEntry() + } + private val _repositoryState = MutableStateFlow(RepositoryState.SAFE) val repositoryState: StateFlow = _repositoryState @@ -153,12 +163,10 @@ class TabViewModel @Inject constructor( if (!operationRunning) { // Only update if there isn't any process running safeProcessing(showError = false) { println("Changes detected, loading status") -// val hasUncommitedChanges = statusManager.hasUncommitedChanges.value -// statusManager.loadHasUncommitedChanges(safeGit) -// statusManager.loadStatus(safeGit) - statusViewModel.refresh(safeGit) checkUncommitedChanges() + + updateDiffEntry() } } } @@ -174,16 +182,8 @@ class TabViewModel @Inject constructor( // Update the log only if the uncommitedChanges status has changed if (uncommitedChangesStateChanged) loadLog() - } - suspend fun diffFormat(diffEntryType: DiffEntryType): List { - try { - return diffManager.diffFormat(safeGit, diffEntryType) - } catch (ex: Exception) { - ex.printStackTrace() - checkUncommitedChanges() - return listOf() - } + updateDiffEntry() } fun pull() = managerScope.launch { @@ -279,8 +279,12 @@ class TabViewModel @Inject constructor( } } - fun updatedDiffEntry(diffSelected: DiffEntryType) = tabState.runOperation { git -> - diffViewModel.updateDiff(git , diffSelected) + fun updateDiffEntry() = tabState.runOperation { git -> + val diffSelected = diffSelected.value + + if(diffSelected != null) { + diffViewModel.updateDiff(git, diffSelected) + } return@runOperation RefreshType.NONE } From 620fd1022b55fcac08608f29fbcee480affb6105 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Mon, 3 Jan 2022 23:04:04 +0100 Subject: [PATCH 3/5] Commit message is now preserved even after disposing the composable (stored in he VM) --- src/main/kotlin/app/git/StatusManager.kt | 2 -- src/main/kotlin/app/ui/UncommitedChanges.kt | 8 ++++---- src/main/kotlin/app/viewmodels/StatusViewModel.kt | 13 ++++++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/app/git/StatusManager.kt b/src/main/kotlin/app/git/StatusManager.kt index 17a9d11..9d4a9e4 100644 --- a/src/main/kotlin/app/git/StatusManager.kt +++ b/src/main/kotlin/app/git/StatusManager.kt @@ -85,8 +85,6 @@ class StatusManager @Inject constructor( dirCacheEditor.commit() completedWithErrors = false - -// loadStatus(git) } finally { if (completedWithErrors) dirCache.unlock() diff --git a/src/main/kotlin/app/ui/UncommitedChanges.kt b/src/main/kotlin/app/ui/UncommitedChanges.kt index c639af7..0e6daba 100644 --- a/src/main/kotlin/app/ui/UncommitedChanges.kt +++ b/src/main/kotlin/app/ui/UncommitedChanges.kt @@ -51,8 +51,9 @@ fun UncommitedChanges( onUnstagedDiffEntrySelected: (DiffEntry) -> Unit, ) { val stageStatusState = statusViewModel.stageStatus.collectAsState() - val stageStatus = stageStatusState.value + val commitMessage by statusViewModel.commitMessage.collectAsState() + val stageStatus = stageStatusState.value val staged: List val unstaged: List if (stageStatus is StageStatus.Loaded) { @@ -74,11 +75,10 @@ fun UncommitedChanges( unstaged = listOf() // return empty lists if still loading } - var commitMessage by remember { mutableStateOf("") } val doCommit = { statusViewModel.commit(commitMessage) onStagedDiffEntrySelected(null) - commitMessage = "" + statusViewModel.newCommitMessage = "" } val canCommit = commitMessage.isNotEmpty() && staged.isNotEmpty() @@ -158,7 +158,7 @@ fun UncommitedChanges( false }, value = commitMessage, - onValueChange = { commitMessage = it }, + onValueChange = { statusViewModel.newCommitMessage = it }, label = { Text("Write your commit message here", fontSize = 14.sp) }, colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.background), textStyle = TextStyle.Default.copy(fontSize = 14.sp), diff --git a/src/main/kotlin/app/viewmodels/StatusViewModel.kt b/src/main/kotlin/app/viewmodels/StatusViewModel.kt index 6a2e394..30a2ab7 100644 --- a/src/main/kotlin/app/viewmodels/StatusViewModel.kt +++ b/src/main/kotlin/app/viewmodels/StatusViewModel.kt @@ -4,7 +4,6 @@ import app.git.* 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 @@ -19,6 +18,13 @@ class StatusViewModel @Inject constructor( private val _stageStatus = MutableStateFlow(StageStatus.Loaded(listOf(), listOf())) val stageStatus: StateFlow = _stageStatus + private val _commitMessage = MutableStateFlow("") + val commitMessage: StateFlow = _commitMessage + var newCommitMessage: String + get() = commitMessage.value + set(value) { + _commitMessage.value = value + } private val _hasUncommitedChanges = MutableStateFlow(false) val hasUncommitedChanges: StateFlow @@ -37,7 +43,6 @@ class StatusViewModel @Inject constructor( } - fun unstageAll() = tabState.safeProcessing { git -> statusManager.unstageAll(git) @@ -51,14 +56,13 @@ class StatusViewModel @Inject constructor( } - fun resetStaged(diffEntry: DiffEntry) = tabState.runOperation { git -> statusManager.reset(git, diffEntry, staged = true) return@runOperation RefreshType.UNCOMMITED_CHANGES } - fun resetUnstaged(diffEntry: DiffEntry) =tabState.runOperation { git -> + fun resetUnstaged(diffEntry: DiffEntry) = tabState.runOperation { git -> statusManager.reset(git, diffEntry, staged = false) return@runOperation RefreshType.UNCOMMITED_CHANGES @@ -92,7 +96,6 @@ class StatusViewModel @Inject constructor( } - suspend fun refresh(git: Git) = withContext(Dispatchers.IO) { loadStatus(git) loadHasUncommitedChanges(git) From e7de563b281891c100756777b94836ce0e7b917d Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Tue, 4 Jan 2022 17:53:31 +0100 Subject: [PATCH 4/5] Fixed adding a new tab crashing --- src/main/kotlin/app/App.kt | 151 ++++++++++-------- src/main/kotlin/app/AppStateManager.kt | 2 +- src/main/kotlin/app/di/AppComponent.kt | 4 +- .../app/ui/components/RepositoriesTabPanel.kt | 1 + src/main/kotlin/main.kt | 4 +- 5 files changed, 92 insertions(+), 70 deletions(-) diff --git a/src/main/kotlin/app/App.kt b/src/main/kotlin/app/App.kt index c0ac860..11199b2 100644 --- a/src/main/kotlin/app/App.kt +++ b/src/main/kotlin/app/App.kt @@ -10,7 +10,6 @@ import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -28,9 +27,11 @@ import app.theme.AppTheme import app.ui.components.RepositoriesTabPanel import app.ui.components.TabInformation import app.ui.dialogs.SettingsDialog +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject -class Main { +class App { private val appComponent = DaggerAppComponent.create() @Inject @@ -39,84 +40,91 @@ class Main { @Inject lateinit var appPreferences: AppPreferences + private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + init { appComponent.inject(this) println("AppStateManagerReference $appStateManager") - appStateManager.loadRepositoriesTabs() } - fun start() = application { - var isOpen by remember { mutableStateOf(true) } - val theme by appPreferences.themeState.collectAsState() - if (isOpen) { - Window( - title = "Gitnuro", - onCloseRequest = { - isOpen = false - }, - state = rememberWindowState( - placement = WindowPlacement.Maximized, - size = DpSize(1280.dp, 720.dp) - ), - icon = painterResource("logo.svg"), - ) { - var showSettingsDialog by remember { mutableStateOf(false) } - val tabs = mutableStateMapOf() + private val tabsFlow = MutableStateFlow>(emptyList()) - AppTheme(theme = theme) { - Box(modifier = Modifier.background(MaterialTheme.colors.background)) { - AppTabs( - tabs = tabs, - onOpenSettings = { - showSettingsDialog = true - } - ) - } + fun start(){ + appStateManager.loadRepositoriesTabs() + loadTabs() - if (showSettingsDialog) { - SettingsDialog( - appPreferences = appPreferences, - onDismiss = { showSettingsDialog = false } - ) + application { + var isOpen by remember { mutableStateOf(true) } + val theme by appPreferences.themeState.collectAsState() + + if (isOpen) { + Window( + title = "Gitnuro", + onCloseRequest = { + isOpen = false + }, + state = rememberWindowState( + placement = WindowPlacement.Maximized, + size = DpSize(1280.dp, 720.dp) + ), + icon = painterResource("logo.svg"), + ) { + var showSettingsDialog by remember { mutableStateOf(false) } + + AppTheme(theme = theme) { + Box(modifier = Modifier.background(MaterialTheme.colors.background)) { + AppTabs( + onOpenSettings = { + showSettingsDialog = true + } + ) + } + + if (showSettingsDialog) { + SettingsDialog( + appPreferences = appPreferences, + onDismiss = { showSettingsDialog = false } + ) + } } } + } else { + appScope.cancel("Closing app") + this.exitApplication() } } } + private fun loadTabs() { + val repositoriesSavedTabs = appStateManager.openRepositoriesPathsTabs + var repoTabs = repositoriesSavedTabs.map { repositoryTab -> + newAppTab( + key = repositoryTab.key, + path = repositoryTab.value + ) + } + + if (repoTabs.isEmpty()) { + repoTabs = listOf( + newAppTab() + ) + } + + tabsFlow.value = repoTabs + + println("After reading prefs, got ${tabsFlow.value.count()} tabs") + } + @Composable fun AppTabs( - tabs: SnapshotStateMap, onOpenSettings: () -> Unit, ) { - - val tabsInformationList = tabs.map { it.value }.sortedBy { it.key } + val tabs by tabsFlow.collectAsState() + val tabsInformationList = tabs.sortedBy { it.key } println("Tabs count ${tabs.count()}") - LaunchedEffect(Unit) { - val repositoriesSavedTabs = appStateManager.openRepositoriesPathsTabs - var repoTabs = repositoriesSavedTabs.map { repositoryTab -> - newAppTab( - key = repositoryTab.key, - path = repositoryTab.value - ) - } - - if (repoTabs.isEmpty()) { - repoTabs = listOf( - newAppTab() - ) - } - - repoTabs.forEach { - tabs[it.key] = it - } // Store list of tabs in the map - - println("After reading prefs, got ${tabs.count()} tabs") - } - val selectedTabKey = remember { mutableStateOf(0) } println("Selected tab key: ${selectedTabKey.value}") @@ -125,22 +133,36 @@ class Main { modifier = Modifier.background(MaterialTheme.colors.background) ) { Tabs( - tabs = tabs, tabsInformationList = tabsInformationList, selectedTabKey = selectedTabKey, - onOpenSettings = onOpenSettings + onOpenSettings = onOpenSettings, + onAddedTab = { tabInfo -> + addTab(tabs, tabInfo) + }, + onRemoveTab = { key -> + removeTab(tabs, key) + } ) TabsContent(tabsInformationList, selectedTabKey.value) } } + private fun removeTab(tabs: List, key: Int) = appScope.launch(Dispatchers.IO) { + tabsFlow.value = tabs.filter { tab -> tab.key != key } + } + + fun addTab(tabsList: List, tabInformation: TabInformation) = appScope.launch(Dispatchers.IO) { + tabsFlow.value = tabsList.toMutableList().apply { add(tabInformation) } + } + @Composable fun Tabs( - tabs: SnapshotStateMap, selectedTabKey: MutableState, onOpenSettings: () -> Unit, tabsInformationList: List, + onAddedTab: (TabInformation) -> Unit, + onRemoveTab: (Int) -> Unit, ) { Row( modifier = Modifier @@ -162,13 +184,12 @@ class Main { key = key ) - tabs[key] = newAppTab - + onAddedTab(newAppTab) newAppTab }, onTabClosed = { key -> appStateManager.repositoryTabRemoved(key) - tabs.remove(key) + onRemoveTab(key) } ) IconButton( diff --git a/src/main/kotlin/app/AppStateManager.kt b/src/main/kotlin/app/AppStateManager.kt index 0f141c6..1246267 100644 --- a/src/main/kotlin/app/AppStateManager.kt +++ b/src/main/kotlin/app/AppStateManager.kt @@ -57,7 +57,7 @@ class AppStateManager @Inject constructor( appPreferences.latestOpenedRepositoriesPath = Json.encodeToString(_latestOpenedRepositoriesPaths) } - fun loadRepositoriesTabs() = appStateScope.launch(Dispatchers.IO) { + fun loadRepositoriesTabs() { val repositoriesSaved = appPreferences.latestTabsOpened if (repositoriesSaved.isNotEmpty()) { diff --git a/src/main/kotlin/app/di/AppComponent.kt b/src/main/kotlin/app/di/AppComponent.kt index 19b6a2c..78eed0b 100644 --- a/src/main/kotlin/app/di/AppComponent.kt +++ b/src/main/kotlin/app/di/AppComponent.kt @@ -1,13 +1,13 @@ package app.di import app.AppStateManager -import app.Main +import app.App import dagger.Component import javax.inject.Singleton @Singleton @Component interface AppComponent { - fun inject(main: Main) + fun inject(main: App) fun appStateManager(): AppStateManager } \ No newline at end of file diff --git a/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt b/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt index b9d5733..ee1de86 100644 --- a/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt +++ b/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt @@ -179,6 +179,7 @@ class TabInformation( .build() tabComponent.inject(this) + //TODO: This shouldn't be here, should be in the parent method gitManager.onRepositoryChanged = { path -> if (path == null) { appStateManager.repositoryTabRemoved(key) diff --git a/src/main/kotlin/main.kt b/src/main/kotlin/main.kt index f7537dc..f346ada 100644 --- a/src/main/kotlin/main.kt +++ b/src/main/kotlin/main.kt @@ -1,8 +1,8 @@ import androidx.compose.ui.ExperimentalComposeUiApi -import app.Main +import app.App @OptIn(ExperimentalComposeUiApi::class) fun main() { - val main = Main() + val main = App() main.start() } \ No newline at end of file From 97a082bc475059abcb37bcea81fdb76c1ddf90ae Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Tue, 4 Jan 2022 19:54:56 +0100 Subject: [PATCH 5/5] Completed arch refactor --- build.gradle.kts | 2 +- src/main/kotlin/app/App.kt | 13 +- src/main/kotlin/app/git/StashManager.kt | 23 +- src/main/kotlin/app/git/TabState.kt | 32 ++- src/main/kotlin/app/ui/AppTab.kt | 41 +-- src/main/kotlin/app/ui/CommitChanges.kt | 37 ++- src/main/kotlin/app/ui/{GMenu.kt => Menu.kt} | 20 +- src/main/kotlin/app/ui/RepositoryOpen.kt | 30 +- src/main/kotlin/app/ui/Stashes.kt | 8 +- src/main/kotlin/app/ui/SystemDialogs.kt | 4 +- src/main/kotlin/app/ui/WelcomePage.kt | 10 +- .../app/ui/components/RepositoriesTabPanel.kt | 14 +- .../app/viewmodels/CommitChangesViewModel.kt | 35 +++ .../kotlin/app/viewmodels/DiffViewModel.kt | 11 +- .../kotlin/app/viewmodels/MenuViewModel.kt | 41 +++ .../kotlin/app/viewmodels/StashesViewModel.kt | 32 +++ .../kotlin/app/viewmodels/StatusViewModel.kt | 16 +- .../kotlin/app/viewmodels/TabViewModel.kt | 258 +++++++----------- .../kotlin/app/viewmodels/TagsViewModel.kt | 2 +- 19 files changed, 335 insertions(+), 294 deletions(-) rename src/main/kotlin/app/ui/{GMenu.kt => Menu.kt} (90%) create mode 100644 src/main/kotlin/app/viewmodels/CommitChangesViewModel.kt create mode 100644 src/main/kotlin/app/viewmodels/MenuViewModel.kt create mode 100644 src/main/kotlin/app/viewmodels/StashesViewModel.kt 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