Merge branch 'architecture_refactor' into main
This commit is contained in:
commit
128397ae26
@ -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)
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()) {
|
||||
|
@ -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
|
||||
}
|
10
src/main/kotlin/app/di/TabComponent.kt
Normal file
10
src/main/kotlin/app/di/TabComponent.kt
Normal 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)
|
||||
}
|
7
src/main/kotlin/app/di/TabScope.kt
Normal file
7
src/main/kotlin/app/di/TabScope.kt
Normal file
@ -0,0 +1,7 @@
|
||||
package app.di
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class TabScope
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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()
|
||||
@ -107,8 +84,3 @@ enum class ResetType {
|
||||
MIXED,
|
||||
HARD,
|
||||
}
|
||||
|
||||
sealed class LogStatus {
|
||||
object Loading : LogStatus()
|
||||
class Loaded(val plotCommitList: GraphCommitList, val currentBranch: Ref?) : LogStatus()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
12
src/main/kotlin/app/git/RepositoryManager.kt
Normal file
12
src/main/kotlin/app/git/RepositoryManager.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
|
122
src/main/kotlin/app/git/TabState.kt
Normal file
122
src/main/kotlin/app/git/TabState.kt
Normal 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,
|
||||
}
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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))
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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>,
|
||||
|
@ -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) {
|
||||
//
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
60
src/main/kotlin/app/viewmodels/BranchesViewModel.kt
Normal file
60
src/main/kotlin/app/viewmodels/BranchesViewModel.kt
Normal 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)
|
||||
}
|
||||
}
|
35
src/main/kotlin/app/viewmodels/CommitChangesViewModel.kt
Normal file
35
src/main/kotlin/app/viewmodels/CommitChangesViewModel.kt
Normal 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()
|
||||
}
|
||||
|
64
src/main/kotlin/app/viewmodels/DiffViewModel.kt
Normal file
64
src/main/kotlin/app/viewmodels/DiffViewModel.kt
Normal 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>)
|
97
src/main/kotlin/app/viewmodels/LogViewModel.kt
Normal file
97
src/main/kotlin/app/viewmodels/LogViewModel.kt
Normal 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()
|
||||
}
|
41
src/main/kotlin/app/viewmodels/MenuViewModel.kt
Normal file
41
src/main/kotlin/app/viewmodels/MenuViewModel.kt
Normal 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
|
||||
}
|
||||
}
|
44
src/main/kotlin/app/viewmodels/RemotesViewModel.kt
Normal file
44
src/main/kotlin/app/viewmodels/RemotesViewModel.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
32
src/main/kotlin/app/viewmodels/StashesViewModel.kt
Normal file
32
src/main/kotlin/app/viewmodels/StashesViewModel.kt
Normal 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()
|
||||
}
|
121
src/main/kotlin/app/viewmodels/StatusViewModel.kt
Normal file
121
src/main/kotlin/app/viewmodels/StatusViewModel.kt
Normal 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()
|
||||
}
|
||||
|
244
src/main/kotlin/app/viewmodels/TabViewModel.kt
Normal file
244
src/main/kotlin/app/viewmodels/TabViewModel.kt
Normal 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()
|
||||
}
|
45
src/main/kotlin/app/viewmodels/TagsViewModel.kt
Normal file
45
src/main/kotlin/app/viewmodels/TagsViewModel.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user