diff --git a/build.gradle.kts b/build.gradle.kts index 5f5d373..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" } @@ -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..b86a7fb 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 @@ -24,105 +23,108 @@ 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 kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject -import javax.inject.Provider -class Main { +class App { private val appComponent = DaggerAppComponent.create() - @Inject - lateinit var gitManagerProvider: Provider - @Inject lateinit var appStateManager: AppStateManager @Inject lateinit var appPreferences: AppPreferences + private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + init { appComponent.inject(this) - - appStateManager.loadRepositoriesTabs() + println("AppStateManagerReference $appStateManager") } - 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}") @@ -131,22 +133,44 @@ 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) { + // 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 } + } + + 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 @@ -168,14 +192,10 @@ class Main { key = key ) - tabs[key] = newAppTab - + onAddedTab(newAppTab) newAppTab }, - onTabClosed = { key -> - appStateManager.repositoryTabRemoved(key) - tabs.remove(key) - } + onTabClosed = onRemoveTab ) IconButton( modifier = Modifier @@ -200,19 +220,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/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 97578d8..78eed0b 100644 --- a/src/main/kotlin/app/di/AppComponent.kt +++ b/src/main/kotlin/app/di/AppComponent.kt @@ -1,11 +1,13 @@ package app.di -import app.Main +import app.AppStateManager +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/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..8af5ea4 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 @@ -12,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. */ @@ -32,17 +27,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 +39,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 +77,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/GitManager.kt b/src/main/kotlin/app/git/GitManager.kt deleted file mode 100644 index e2f46d3..0000000 --- a/src/main/kotlin/app/git/GitManager.kt +++ /dev/null @@ -1,413 +0,0 @@ -package app.git - -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 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.Ref -import org.eclipse.jgit.lib.Repository -import org.eclipse.jgit.lib.RepositoryState -import org.eclipse.jgit.revwalk.RevCommit -import org.eclipse.jgit.storage.file.FileRepositoryBuilder -import java.io.File -import javax.inject.Inject - - -class GitManager @Inject constructor( - private val statusManager: StatusManager, - private val logManager: LogManager, - private val remoteOperationsManager: RemoteOperationsManager, - private val branchesManager: BranchesManager, - private val stashManager: StashManager, - private val diffManager: DiffManager, - private val tagsManager: TagsManager, - private val remotesManager: RemotesManager, - val errorsManager: ErrorsManager, - val appStateManager: AppStateManager, - private val fileChangesWatcher: FileChangesWatcher, -) { - val repositoryName: String - get() = safeGit.repository.directory.parentFile.name - - 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 - - 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 - - /** - * Property that indicates if a git operation is running - */ - @set:Synchronized private var operationRunning = false - - private val safeGit: Git - get() { - val git = this.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}") - - val gitDirectory = if (directory.name == ".git") { - 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) - git = Git(repository) - - onRepositoryChanged(repository.directory.parent) - refreshRepositoryInfo() - launch { - watchRepositoryChanges() - } - } catch (ex: Exception) { - ex.printStackTrace() - onRepositoryChanged(null) - errorsManager.addError(newErrorNow(ex, ex.localizedMessage)) - } - } - } - - private suspend fun watchRepositoryChanges() { - val ignored = safeGit.status().call().ignoredNotInIndex.toList() - - fileChangesWatcher.watchDirectoryPath( - pathStr = safeGit.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") - val hasUncommitedChanges = statusManager.hasUncommitedChanges.value - statusManager.loadHasUncommitedChanges(safeGit) - statusManager.loadStatus(safeGit) - - if(!hasUncommitedChanges) { - logManager.loadLog(safeGit) - } - } - } - } - } - - fun loadLog() = managerScope.launch { - coLoadLog() - } - - private suspend fun coLoadLog() { - logManager.loadLog(safeGit) - } - - suspend fun loadStatus() { - val hadUncommitedChanges = statusManager.hasUncommitedChanges.value - - statusManager.loadStatus(safeGit) - - val hasNowUncommitedChanges = statusManager.hasUncommitedChanges.value - - // Update the log only if the uncommitedChanges status has changed - if (hasNowUncommitedChanges != hadUncommitedChanges) - coLoadLog() - } - - 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() - return listOf() - } - } - - fun pull() = managerScope.launch { - safeProcessing { - remoteOperationsManager.pull(safeGit) - coLoadLog() - } - } - - fun push() = managerScope.launch { - safeProcessing { - try { - remoteOperationsManager.push(safeGit) - } finally { - coLoadLog() - } - } - } - - 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) - stashManager.loadStashList(safeGit) - coLoadLog() - } - - fun stash() = managerScope.launch { - safeProcessing { - stashManager.stash(safeGit) - loadStatus() - loadLog() - } - } - - fun popStash() = managerScope.launch { - safeProcessing { - stashManager.popStash(safeGit) - loadStatus() - 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) - } - - fun httpCredentialsAccepted(user: String, password: String) { - credentialsStateManager.updateState(CredentialsState.HttpCredentialsAccepted(user, password)) - } - - fun sshCredentialsAccepted(password: String) { - credentialsStateManager.updateState(CredentialsState.SshCredentialsAccepted(password)) - } - - suspend fun diffListFromCommit(commit: RevCommit): List { - 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() - } - - fun clone(directory: File, url: String) = managerScope.launch { - remoteOperationsManager.clone(directory, url) - } - - fun findCommit(objectId: ObjectId): RevCommit { - return safeGit.repository.parseCommit(objectId) - } - - @Synchronized - 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 - } - } - - private inline fun runOperation(block: () -> Unit) { - operationRunning = true - try { - block() - } finally { - operationRunning = false - } - } -} - - -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/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/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/StatusManager.kt b/src/main/kotlin/app/git/StatusManager.kt index d6d8cee..9d4a9e4 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) { @@ -174,8 +85,6 @@ class StatusManager @Inject constructor( dirCacheEditor.commit() completedWithErrors = false - - loadStatus(git) } finally { if (completedWithErrors) dirCache.unlock() @@ -226,7 +135,7 @@ class StatusManager @Inject constructor( completedWithErrors = false - loadStatus(git) +// loadStatus(git) } finally { if (completedWithErrors) dirCache.unlock() @@ -271,8 +180,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 +187,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 +202,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 +216,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..8628767 --- /dev/null +++ b/src/main/kotlin/app/git/TabState.kt @@ -0,0 +1,122 @@ +package app.git + +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 +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 = _processing + + fun safeProcessing(showError: Boolean = true, callback: suspend (git: Git) -> RefreshType) = + managerScope.launch(Dispatchers.IO) { + 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 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) + + 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..6ffee10 100644 --- a/src/main/kotlin/app/ui/AppTab.kt +++ b/src/main/kotlin/app/ui/AppTab.kt @@ -21,39 +21,21 @@ 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.RepositorySelectionStatus +import app.viewmodels.TabViewModel +import app.viewmodels.RepositorySelectionStatus 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: GitManager, - 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(gitManager = gitManager) + RepositoryOpenPage(tabViewModel = tabViewModel) } } } @@ -158,7 +135,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..1962da0 100644 --- a/src/main/kotlin/app/ui/Branches.kt +++ b/src/main/kotlin/app/ui/Branches.kt @@ -13,23 +13,22 @@ 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.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 +47,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 +61,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..67cbd34 100644 --- a/src/main/kotlin/app/ui/CommitChanges.kt +++ b/src/main/kotlin/app/ui/CommitChanges.kt @@ -3,23 +3,18 @@ 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 -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 -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.viewmodels.TabViewModel import app.theme.headerBackground import app.theme.headerText import app.theme.primaryTextColor @@ -27,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: GitManager, - 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(), @@ -92,7 +105,7 @@ fun CommitChanges( ) - CommitLogChanges(diff, onDiffSelected = onDiffSelected) + CommitLogChanges(changes, onDiffSelected = onDiffSelected) } } } diff --git a/src/main/kotlin/app/ui/Diff.kt b/src/main/kotlin/app/ui/Diff.kt index e6dbd3a..667b745 100644 --- a/src/main/kotlin/app/ui/Diff.kt +++ b/src/main/kotlin/app/ui/Diff.kt @@ -16,24 +16,23 @@ 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.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 @@ -54,12 +53,13 @@ fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView: Text("Close diff") } + val scrollState by diffViewModel.lazyListState.collectAsState() ScrollableLazyColumn( modifier = Modifier - .fillMaxSize() -// .padding(16.dp) + .fillMaxSize(), + state = scrollState ) { - itemsIndexed(text) { index, hunk -> + itemsIndexed(hunks) { index, hunk -> val hunksSeparation = if (index == 0) 0.dp else @@ -96,9 +96,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/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/Remotes.kt b/src/main/kotlin/app/ui/Remotes.kt index 708f784..f1f3160 100644 --- a/src/main/kotlin/app/ui/Remotes.kt +++ b/src/main/kotlin/app/ui/Remotes.kt @@ -12,16 +12,16 @@ 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.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..1dd8b52 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.GitManager +import app.viewmodels.TabViewModel import app.ui.dialogs.NewBranchDialog import app.ui.log.Log import openRepositoryDialog @@ -18,17 +17,14 @@ import org.jetbrains.compose.splitpane.rememberSplitPaneState @OptIn(ExperimentalSplitPaneApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class) @Composable -fun RepositoryOpenPage(gitManager: GitManager) { - var diffSelected by remember { - mutableStateOf(null) - } +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) { - diffSelected = null + tabViewModel.newDiffSelected = null } if (showNewBranchDialog) { @@ -37,21 +33,18 @@ fun RepositoryOpenPage(gitManager: GitManager) { showNewBranchDialog = false }, onAccept = { branchName -> - gitManager.createBranch(branchName) + tabViewModel.branchesViewModel.createBranch(branchName) showNewBranchDialog = false } ) } Column { - GMenu( + Menu( + menuViewModel = tabViewModel.menuViewModel, onRepositoryOpen = { - openRepositoryDialog(gitManager = gitManager) + openRepositoryDialog(gitManager = tabViewModel) }, - onPull = { gitManager.pull() }, - onPush = { gitManager.push() }, - onStash = { gitManager.stash() }, - onPopStash = { gitManager.popStash() }, onCreateBranch = { showNewBranchDialog = true } ) @@ -65,24 +58,22 @@ fun RepositoryOpenPage(gitManager: GitManager) { .fillMaxHeight() ) { Branches( - gitManager = gitManager, + branchesViewModel = tabViewModel.branchesViewModel, onBranchClicked = { - val commit = gitManager.findCommit(it.objectId) - setSelectedItem(SelectedItem.Ref(commit)) + tabViewModel.newSelectedRef(it.objectId) } ) - Remotes(gitManager = gitManager) + Remotes(remotesViewModel = tabViewModel.remotesViewModel) Tags( - gitManager = gitManager, + tagsViewModel = tabViewModel.tagsViewModel, onTagClicked = { - val commit = gitManager.findCommit(it.objectId) - setSelectedItem(SelectedItem.Ref(commit)) + tabViewModel.newSelectedRef(it.objectId) } ) Stashes( - gitManager = gitManager, + stashesViewModel = tabViewModel.stashesViewModel, onStashSelected = { stash -> - setSelectedItem(SelectedItem.Stash(stash)) + tabViewModel.newSelectedStash(stash) } ) } @@ -97,23 +88,22 @@ fun RepositoryOpenPage(gitManager: GitManager) { modifier = Modifier .fillMaxSize() ) { - Crossfade(targetState = diffSelected) { diffEntry -> - when (diffEntry) { - null -> { - Log( - gitManager = gitManager, - selectedItem = selectedItem, - onItemSelected = { - setSelectedItem(it) - }, - ) - } - else -> { - Diff( - gitManager = gitManager, - diffEntryType = diffEntry, - onCloseDiffView = { diffSelected = null }) - } + when (diffSelected) { + null -> { + Log( + tabViewModel = tabViewModel, + repositoryState = repositoryState, + logViewModel = tabViewModel.logViewModel, + selectedItem = selectedItem, + onItemSelected = { + tabViewModel.newSelectedItem(it) + }, + ) + } + else -> { + Diff( + diffViewModel = tabViewModel.diffViewModel, + onCloseDiffView = { tabViewModel.newDiffSelected = null }) } } } @@ -124,26 +114,27 @@ fun RepositoryOpenPage(gitManager: GitManager) { modifier = Modifier .fillMaxHeight() ) { - if (selectedItem == SelectedItem.UncommitedChanges) { + val safeSelectedItem = selectedItem + if (safeSelectedItem == SelectedItem.UncommitedChanges) { UncommitedChanges( - gitManager = gitManager, + statusViewModel = tabViewModel.statusViewModel, 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) { + } else if (safeSelectedItem is SelectedItem.CommitBasedItem) { CommitChanges( - gitManager = gitManager, - commit = selectedItem.revCommit, + commitChangesViewModel = tabViewModel.commitChangesViewModel, 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 3337bce..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.git.GitManager -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: GitManager, + 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 6579030..c43e045 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.viewmodels.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, + 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/Tags.kt b/src/main/kotlin/app/ui/Tags.kt index 4c8d4d5..e1f77d0 100644 --- a/src/main/kotlin/app/ui/Tags.kt +++ b/src/main/kotlin/app/ui/Tags.kt @@ -13,20 +13,20 @@ 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.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 +46,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..0e6daba 100644 --- a/src/main/kotlin/app/ui/UncommitedChanges.kt +++ b/src/main/kotlin/app/ui/UncommitedChanges.kt @@ -28,37 +28,32 @@ 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 commitMessage by statusViewModel.commitMessage.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 if (stageStatus is StageStatus.Loaded) { @@ -80,12 +75,10 @@ fun UncommitedChanges( unstaged = listOf() // return empty lists if still loading } - - var commitMessage by remember { mutableStateOf("") } val doCommit = { - gitManager.commit(commitMessage) + statusViewModel.commit(commitMessage) onStagedDiffEntrySelected(null) - commitMessage = "" + statusViewModel.newCommitMessage = "" } val canCommit = commitMessage.isNotEmpty() && staged.isNotEmpty() @@ -111,13 +104,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 +125,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" ) @@ -165,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), @@ -196,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 501cc9d..c66b7f2 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.viewmodels.TabViewModel import app.theme.primaryTextColor import app.theme.secondaryTextColor import app.ui.dialogs.CloneDialog @@ -33,9 +33,9 @@ import java.net.URI @OptIn(ExperimentalMaterialApi::class) @Composable fun WelcomePage( - gitManager: GitManager, + 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 377d3fb..ddd020a 100644 --- a/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt +++ b/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt @@ -20,8 +20,16 @@ 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.viewmodels.TabViewModel 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 @@ -48,7 +56,7 @@ fun RepositoriesTabPanel( ) { items(items = tabs) { tab -> Tab( - title = tab.title, + title = tab.tabName, selected = tab.key == selectedTabKey, onClick = { onTabSelected(tab.key) @@ -154,7 +162,38 @@ 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 tabViewModel: 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) + + //TODO: This shouldn't be here, should be in the parent method + tabViewModel.onRepositoryChanged = { path -> + if (path == null) { + appStateManager.repositoryTabRemoved(key) + } else { + tabName.value = Path(path).name + appStateManager.repositoryTabChanged(key, path) + } + } + if(path != null) + tabViewModel.openRepository(path) + content = { + AppTab(tabViewModel) + } + } +} \ 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..53a7703 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.viewmodels.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..b480a3c 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.viewmodels.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/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 new file mode 100644 index 0000000..7d7457d --- /dev/null +++ b/src/main/kotlin/app/viewmodels/DiffViewModel.kt @@ -0,0 +1,64 @@ +package app.viewmodels + +import androidx.compose.foundation.lazy.LazyListState +import app.git.* +import app.git.diff.Hunk +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +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 + + val lazyListState = MutableStateFlow( + LazyListState( + 0, + 0 + ) + ) + + fun updateDiff(diffEntryType: DiffEntryType) = tabState.runOperation { git -> + 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) + + return@runOperation RefreshType.NONE + } + + 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/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/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/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 new file mode 100644 index 0000000..41828a1 --- /dev/null +++ b/src/main/kotlin/app/viewmodels/StatusViewModel.kt @@ -0,0 +1,121 @@ +package app.viewmodels + +import app.git.* +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.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 _commitMessage = MutableStateFlow("") + val commitMessage: StateFlow = _commitMessage + var newCommitMessage: String + get() = commitMessage.value + set(value) { + _commitMessage.value = value + } + + private var lastUncommitedChangesState = false + + fun stage(diffEntry: DiffEntry) = tabState.runOperation { git -> + statusManager.stage(git, diffEntry) + + return@runOperation RefreshType.UNCOMMITED_CHANGES + } + + fun unstage(diffEntry: DiffEntry) = tabState.runOperation { git -> + statusManager.unstage(git, diffEntry) + + return@runOperation RefreshType.UNCOMMITED_CHANGES + } + + + fun unstageAll() = tabState.safeProcessing { git -> + statusManager.unstageAll(git) + + return@safeProcessing RefreshType.UNCOMMITED_CHANGES + } + + fun stageAll() = tabState.safeProcessing { git -> + statusManager.stageAll(git) + + return@safeProcessing RefreshType.UNCOMMITED_CHANGES + } + + + 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 + } + + private 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 + } + } + + private suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) { + lastUncommitedChangesState = 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 = this.lastUncommitedChangesState + + loadStatus(git) + loadHasUncommitedChanges(git) + + val hasNowUncommitedChanges = this.lastUncommitedChangesState + + // 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/viewmodels/TabViewModel.kt b/src/main/kotlin/app/viewmodels/TabViewModel.kt new file mode 100644 index 0000000..53fe306 --- /dev/null +++ b/src/main/kotlin/app/viewmodels/TabViewModel.kt @@ -0,0 +1,244 @@ +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.* +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.lib.ObjectId +import org.eclipse.jgit.lib.Repository +import org.eclipse.jgit.lib.RepositoryState +import org.eclipse.jgit.revwalk.RevCommit +import org.eclipse.jgit.storage.file.FileRepositoryBuilder +import java.io.File +import javax.inject.Inject + + +class TabViewModel @Inject constructor( + val logViewModel: LogViewModel, + val branchesViewModel: BranchesViewModel, + val tagsViewModel: TagsViewModel, + 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 tabState: TabState, + val errorsManager: ErrorsManager, + val appStateManager: AppStateManager, + private val fileChangesWatcher: FileChangesWatcher, +) { + private val _selectedItem = MutableStateFlow(SelectedItem.None) + val selectedItem: StateFlow = _selectedItem + + private val credentialsStateManager = CredentialsStateManager + + private val _repositorySelectionStatus = MutableStateFlow(RepositorySelectionStatus.None) + val repositorySelectionStatus: StateFlow + get() = _repositorySelectionStatus + + val processing: StateFlow = tabState.processing + + 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 + + init { + 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(false) + } + } + } + } + + private fun refreshLog() = tabState.runOperation { git -> + logViewModel.refresh(git) + + return@runOperation RefreshType.NONE + } + + fun openRepository(directory: String) { + openRepository(File(directory)) + } + + fun openRepository(directory: File) = tabState.safeProcessingWihoutGit { + println("Trying to open repository ${directory.absoluteFile}") + + val gitDirectory = if (directory.name == ".git") { + 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) + 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 + } + + private suspend fun loadRepositoryState(git: Git) = withContext(Dispatchers.IO) { + _repositoryState.value = repositoryManager.getRepositoryState(git) + } + + private suspend fun watchRepositoryChanges(git: Git) = tabState.managerScope.launch(Dispatchers.IO) { + val ignored = git.status().call().ignoredNotInIndex.toList() + + fileChangesWatcher.watchDirectoryPath( + pathStr = git.repository.directory.parent, + ignoredDirsPath = ignored, + ).collect { + if (!tabState.operationRunning) { // Only update if there isn't any process running + println("Changes detected, loading status") + checkUncommitedChanges(isFsChange = true) + + updateDiffEntry() + } + } + } + + 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 && 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 + } + + 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) + + return@safeProcessing RefreshType.NONE + } + + fun credentialsDenied() { + credentialsStateManager.updateState(CredentialsState.CredentialsDenied) + } + + fun httpCredentialsAccepted(user: String, password: String) { + credentialsStateManager.updateState(CredentialsState.HttpCredentialsAccepted(user, password)) + } + + fun sshCredentialsAccepted(password: String) { + credentialsStateManager.updateState(CredentialsState.SshCredentialsAccepted(password)) + } + + var onRepositoryChanged: (path: String?) -> Unit = {} + + + fun dispose() { + tabState.managerScope.cancel() + } + + fun clone(directory: File, url: String) = tabState.safeProcessingWihoutGit { + remoteOperationsManager.clone(directory, url) + + return@safeProcessingWihoutGit RefreshType.NONE + } + + private fun findCommit(git: Git, objectId: ObjectId): RevCommit { + return git.repository.parseCommit(objectId) + } + + private fun updateDiffEntry() { + val diffSelected = diffSelected.value + + 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) + } + } +} + + +sealed class RepositorySelectionStatus { + object None : RepositorySelectionStatus() + object Loading : RepositorySelectionStatus() + data class Open(val repository: Repository) : RepositorySelectionStatus() +} diff --git a/src/main/kotlin/app/viewmodels/TagsViewModel.kt b/src/main/kotlin/app/viewmodels/TagsViewModel.kt new file mode 100644 index 0000000..25e8ab9 --- /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 + + private 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 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