Merge branch 'architecture_refactor' into main

This commit is contained in:
Abdelilah El Aissaoui 2022-01-04 19:55:07 +01:00
commit 128397ae26
41 changed files with 1338 additions and 912 deletions

View File

@ -36,7 +36,7 @@ dependencies {
tasks.withType<KotlinCompile>() {
kotlinOptions.jvmTarget = "11"
kotlinOptions.allWarningsAsErrors = true
kotlinOptions.allWarningsAsErrors = false
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
@ -44,7 +44,7 @@ tasks.withType<KotlinCompile>() {
compose.desktop {
application {
mainClass = "MainKt"
//
nativeDistributions {
includeAllModules = true
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.AppImage)

View File

@ -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<GitManager>
@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<Int, TabInformation>()
private val tabsFlow = MutableStateFlow<List<TabInformation>>(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<Int, TabInformation>,
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<TabInformation>, 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: TabInformation) = appScope.launch(Dispatchers.IO) {
tabsFlow.value = tabsList.toMutableList().apply { add(tabInformation) }
}
@Composable
fun Tabs(
tabs: SnapshotStateMap<Int, TabInformation>,
selectedTabKey: MutableState<Int>,
onOpenSettings: () -> Unit,
tabsInformationList: List<TabInformation>,
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,
)
}
}

View File

@ -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()) {

View File

@ -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
}

View File

@ -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)
}

View File

@ -0,0 +1,7 @@
package app.di
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class TabScope

View File

@ -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<List<Ref>>(listOf())
val branches: StateFlow<List<Ref>>
get() = _branches
private val _currentBranch = MutableStateFlow<String>("")
val currentBranch: StateFlow<String>
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()
}
}
}

View File

@ -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>(RepositorySelectionStatus.None)
val repositorySelectionStatus: StateFlow<RepositorySelectionStatus>
get() = _repositorySelectionStatus
private val _processing = MutableStateFlow(false)
val processing: StateFlow<Boolean>
get() = _processing
private val _lastTimeChecked = MutableStateFlow(System.currentTimeMillis())
val lastTimeChecked: StateFlow<Long>
get() = _lastTimeChecked
val stageStatus: StateFlow<StageStatus> = statusManager.stageStatus
val repositoryState: StateFlow<RepositoryState> = statusManager.repositoryState
val logStatus: StateFlow<LogStatus> = logManager.logStatus
val branches: StateFlow<List<Ref>> = branchesManager.branches
val tags: StateFlow<List<Ref>> = tagsManager.tags
val currentBranch: StateFlow<String> = branchesManager.currentBranch
val stashStatus: StateFlow<StashStatus> = stashManager.stashStatus
val credentialsState: StateFlow<CredentialsState> = credentialsStateManager.credentialsState
val cloneStatus: StateFlow<CloneStatus> = remoteOperationsManager.cloneStatus
val remotes: StateFlow<List<RemoteInfo>> = 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<Boolean>
get() = statusManager.hasUncommitedChanges
suspend fun diffFormat(diffEntryType: DiffEntryType): List<Hunk> {
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<DiffEntry> {
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()
}

View File

@ -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>(LogStatus.Loading)
val logStatus: StateFlow<LogStatus>
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()
}

View File

@ -10,22 +10,18 @@ import org.eclipse.jgit.transport.RemoteConfig
import javax.inject.Inject
class RemotesManager @Inject constructor() {
private val _remotes = MutableStateFlow<List<RemoteInfo>>(listOf())
val remotes: StateFlow<List<RemoteInfo>>
get() = _remotes
suspend fun loadRemotes(git: Git, allRemoteBranches: List<Ref>) = 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
}
}

View File

@ -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
}
}

View File

@ -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>(StashStatus.Loaded(listOf()))
val stashStatus: StateFlow<StashStatus>
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<RevCommit>) : StashStatus()
}

View File

@ -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>(StageStatus.Loaded(listOf(), listOf()))
val stageStatus: StateFlow<StageStatus> = _stageStatus
private val _repositoryState = MutableStateFlow(RepositoryState.SAFE)
val repositoryState: StateFlow<RepositoryState> = _repositoryState
private val _hasUncommitedChanges = MutableStateFlow<Boolean>(false)
val hasUncommitedChanges: StateFlow<Boolean>
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<StatusEntry>, val unstaged: List<StatusEntry>) : StageStatus()
}
data class StatusEntry(val diffEntry: DiffEntry, val isConflict: Boolean) {
val icon: ImageVector

View File

@ -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<RefreshType>()
val refreshData: Flow<RefreshType> = _refreshData
suspend fun refreshData(refreshType: RefreshType) = _refreshData.emit(refreshType)
private val _errors = MutableSharedFlow<Error>()
val errors: Flow<Error> = _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<Boolean> = _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,
}

View File

@ -10,16 +10,8 @@ import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
class TagsManager @Inject constructor() {
private val _tags = MutableStateFlow<List<Ref>>(listOf())
val tags: StateFlow<List<Ref>>
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) {

View File

@ -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<String>
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) {

View File

@ -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<Ref?>(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) }
)
}
}

View File

@ -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<DiffEntry>()) }
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<DiffEntry>,
onDiffSelected: (DiffEntry) -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize(),
@ -92,7 +105,7 @@ fun CommitChanges(
)
CommitLogChanges(diff, onDiffSelected = onDiffSelected)
CommitLogChanges(changes, onDiffSelected = onDiffSelected)
}
}
}

View File

@ -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<Hunk>()) }
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)
}
}
)

View File

@ -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))

View File

@ -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")

View File

@ -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<DiffEntryType?>(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>(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)
}
)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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) }
)
}
}

View File

@ -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<StatusEntry>
val unstaged: List<StatusEntry>
if (stageStatus is StageStatus.Loaded) {
@ -80,12 +75,10 @@ fun UncommitedChanges(
unstaged = listOf<StatusEntry>() // 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<StatusEntry>,

View File

@ -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) {
//

View File

@ -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<String>, selected: Boolean, onClick: () -> Unit, onC
}
class TabInformation(
val title: MutableState<String>,
val tabName: MutableState<String>,
val key: Int,
val path: String?,
appComponent: AppComponent,
) {
@Inject
lateinit var tabViewModel: TabViewModel
@Inject
lateinit var appStateManager: AppStateManager
val content: @Composable (TabInformation) -> Unit
)
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)
}
}
}

View File

@ -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()

View File

@ -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>(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<Float>,
@ -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) },
)
}
}

View File

@ -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<List<Ref>>(listOf())
val branches: StateFlow<List<Ref>>
get() = _branches
private val _currentBranch = MutableStateFlow<String>("")
val currentBranch: StateFlow<String>
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)
}
}

View File

@ -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>(CommitChangesStatus.Loading)
val commitChangesStatus: StateFlow<CommitChangesStatus> = _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<DiffEntry>) : CommitChangesStatus()
}

View File

@ -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<DiffResult?>(null)
val diffResult: StateFlow<DiffResult?> = _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<Hunk>)

View File

@ -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>(LogStatus.Loading)
val logStatus: StateFlow<LogStatus>
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()
}

View File

@ -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
}
}

View File

@ -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<List<RemoteInfo>>(listOf())
val remotes: StateFlow<List<RemoteInfo>>
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)
}
}

View File

@ -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>(StashStatus.Loaded(listOf()))
val stashStatus: StateFlow<StashStatus>
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<RevCommit>) : StashStatus()
}

View File

@ -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>(StageStatus.Loaded(listOf(), listOf()))
val stageStatus: StateFlow<StageStatus> = _stageStatus
private val _commitMessage = MutableStateFlow("")
val commitMessage: StateFlow<String> = _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<StatusEntry>, val unstaged: List<StatusEntry>) : StageStatus()
}

View File

@ -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>(SelectedItem.None)
val selectedItem: StateFlow<SelectedItem> = _selectedItem
private val credentialsStateManager = CredentialsStateManager
private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None)
val repositorySelectionStatus: StateFlow<RepositorySelectionStatus>
get() = _repositorySelectionStatus
val processing: StateFlow<Boolean> = tabState.processing
val credentialsState: StateFlow<CredentialsState> = credentialsStateManager.credentialsState
val cloneStatus: StateFlow<CloneStatus> = remoteOperationsManager.cloneStatus
private val _diffSelected = MutableStateFlow<DiffEntryType?>(null)
val diffSelected: StateFlow<DiffEntryType?> = _diffSelected
var newDiffSelected: DiffEntryType?
get() = diffSelected.value
set(value) {
_diffSelected.value = value
updateDiffEntry()
}
private val _repositoryState = MutableStateFlow(RepositoryState.SAFE)
val repositoryState: StateFlow<RepositoryState> = _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()
}

View File

@ -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<List<Ref>>(listOf())
val tags: StateFlow<List<Ref>>
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)
}
}

View File

@ -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()
}