package app.viewmodels import app.AppStateManager import app.ErrorsManager import app.credentials.CredentialsState import app.credentials.CredentialsStateManager import app.git.* import app.newErrorNow 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 private const val MIN_TIME_IN_MS_BETWEEN_REFRESHES = 500L 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 appStateManager: AppStateManager, private val fileChangesWatcher: FileChangesWatcher, ) { val errorsManager: ErrorsManager = tabState.errorsManager val selectedItem: StateFlow = tabState.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.STASHES -> refreshStashes() RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges() RefreshType.UNCOMMITED_CHANGES_AND_LOG -> checkUncommitedChanges(true) RefreshType.REMOTES -> refreshRemotes() } } } } private fun refreshRemotes() = tabState.runOperation( refreshType = RefreshType.NONE ) { git -> remotesViewModel.refresh(git) } private fun refreshStashes() = tabState.runOperation( refreshType = RefreshType.NONE ) { git -> stashesViewModel.refresh(git) } private fun refreshLog() = tabState.runOperation( refreshType = RefreshType.NONE, ) { git -> logViewModel.refresh(git) } 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)) } } 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() var asyncJob: Job? = null var lastNotify = 0L var hasGitDirChanged = false launch { fileChangesWatcher.changesNotifier.collect { latestUpdateChangedGitDir -> if (!tabState.operationRunning) { // Only update if there isn't any process running println("Detected changes in the repository's directory") if(latestUpdateChangedGitDir) { hasGitDirChanged = true } asyncJob?.cancel() // Sometimes external apps can run filesystem multiple operations in a fraction of a second. // To prevent excessive updates, we add a slight delay between updates emission to prevent slowing down // the app by constantly running "git status". val currentTimeMillis = System.currentTimeMillis() val diffTime = currentTimeMillis - lastNotify // When .git dir has changed, do the refresh with a delay to avoid doing operations while a git // operation may be running if (diffTime > MIN_TIME_IN_MS_BETWEEN_REFRESHES && !hasGitDirChanged) { updateApp(false) println("Sync emit with diff time $diffTime") } else { asyncJob = async { delay(MIN_TIME_IN_MS_BETWEEN_REFRESHES) println("Async emit") if (isActive) updateApp(hasGitDirChanged) hasGitDirChanged = false } } lastNotify = currentTimeMillis } else { println("Ignoring changed occurred during operation running...") } } } fileChangesWatcher.watchDirectoryPath( pathStr = git.repository.directory.parent, ignoredDirsPath = ignored, ) } suspend fun updateApp(hasGitDirChanged: Boolean) { if(hasGitDirChanged) { println("Changes detected in git directory, full refresh") refreshRepositoryInfo() } else { println("Changes detected, partial refresh") checkUncommitedChanges() } } private suspend fun checkUncommitedChanges(fullUpdateLog: Boolean = false) = tabState.runOperation( refreshType = RefreshType.NONE, ) { git -> val uncommitedChangesStateChanged = statusViewModel.updateHasUncommitedChanges(git) println("Has uncommitedChangesStateChanged $uncommitedChangesStateChanged") // Update the log only if the uncommitedChanges status has changed or requested if (uncommitedChangesStateChanged || fullUpdateLog) logViewModel.refresh(git) else logViewModel.refreshUncommitedChanges(git) updateDiffEntry() // 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) } private suspend fun refreshRepositoryInfo() = tabState.safeProcessing( refreshType = RefreshType.NONE, ) { git -> loadRepositoryState(git) logViewModel.refresh(git) branchesViewModel.refresh(git) remotesViewModel.refresh(git) tagsViewModel.refresh(git) statusViewModel.refresh(git) stashesViewModel.refresh(git) } 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) } private fun updateDiffEntry() { val diffSelected = diffSelected.value if (diffSelected != null) { diffViewModel.updateDiff(diffSelected) } } } sealed class RepositorySelectionStatus { object None : RepositorySelectionStatus() object Loading : RepositorySelectionStatus() data class Open(val repository: Repository) : RepositorySelectionStatus() }