Creation of multiple viewmodels that hold data state

This commit is contained in:
Abdelilah El Aissaoui 2022-01-03 21:39:53 +01:00
parent 997a651faf
commit 9c53ce726e
33 changed files with 846 additions and 485 deletions

View file

@ -44,7 +44,7 @@ tasks.withType<KotlinCompile>() {
compose.desktop {
application {
mainClass = "MainKt"
//
nativeDistributions {
includeAllModules = true
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.AppImage)

View file

@ -24,21 +24,15 @@ import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import androidx.compose.ui.zIndex
import app.di.DaggerAppComponent
import app.git.GitManager
import app.theme.AppTheme
import app.ui.AppTab
import app.ui.components.RepositoriesTabPanel
import app.ui.components.TabInformation
import app.ui.dialogs.SettingsDialog
import javax.inject.Inject
import javax.inject.Provider
class Main {
private val appComponent = DaggerAppComponent.create()
@Inject
lateinit var gitManagerProvider: Provider<GitManager>
@Inject
lateinit var appStateManager: AppStateManager
@ -47,7 +41,7 @@ class Main {
init {
appComponent.inject(this)
println("AppStateManagerReference $appStateManager")
appStateManager.loadRepositoriesTabs()
}
@ -200,19 +194,11 @@ class Main {
): TabInformation {
return TabInformation(
title = tabName,
key = key
) {
val gitManager = remember { gitManagerProvider.get() }
gitManager.onRepositoryChanged = { path ->
if (path == null) {
appStateManager.repositoryTabRemoved(key)
} else
appStateManager.repositoryTabChanged(key, path)
}
AppTab(gitManager, path, tabName)
}
tabName = tabName,
key = key,
path = path,
appComponent = appComponent,
)
}
}

View file

@ -1,5 +1,6 @@
package app.di
import app.AppStateManager
import app.Main
import dagger.Component
import javax.inject.Singleton
@ -8,4 +9,5 @@ import javax.inject.Singleton
@Component
interface AppComponent {
fun inject(main: Main)
fun appStateManager(): AppStateManager
}

View file

@ -0,0 +1,10 @@
package app.di
import app.ui.components.TabInformation
import dagger.Component
@TabScope
@Component(dependencies = [ AppComponent::class ])
interface TabComponent {
fun inject(tabInformation: TabInformation)
}

View file

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

View file

@ -1,9 +1,12 @@
package app.git
import app.extensions.isBranch
import app.extensions.simpleName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.CreateBranchCommand
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.ListBranchCommand
import org.eclipse.jgit.api.MergeCommand
@ -32,17 +35,6 @@ class BranchesManager @Inject constructor() {
return branchList.firstOrNull { it.name == branchName }
}
suspend fun loadBranches(git: Git) = withContext(Dispatchers.IO) {
val branchList = getBranches(git)
val branchName = git
.repository
.fullBranch
_branches.value = branchList
_currentBranch.value = branchName
}
suspend fun getBranches(git: Git) = withContext(Dispatchers.IO) {
return@withContext git
.branchList()
@ -55,8 +47,6 @@ class BranchesManager @Inject constructor() {
.setCreateBranch(true)
.setName(branchName)
.call()
loadBranches(git)
}
suspend fun createBranchOnCommit(git: Git, branch: String, revCommit: RevCommit) = withContext(Dispatchers.IO) {
@ -95,4 +85,17 @@ class BranchesManager @Inject constructor() {
.setListMode(ListBranchCommand.ListMode.REMOTE)
.call()
}
suspend fun checkoutRef(git: Git, ref: Ref) = withContext(Dispatchers.IO) {
git.checkout().apply {
setName(ref.name)
if (ref.isBranch && ref.name.startsWith("refs/remotes/")) {
setCreateBranch(true)
setName(ref.simpleName)
setStartPoint(ref.objectId.name)
setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK)
}
call()
}
}
}

View file

@ -21,17 +21,8 @@ import javax.inject.Inject
class LogManager @Inject constructor(
private val statusManager: StatusManager,
private val branchesManager: BranchesManager,
) {
private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading)
val logStatus: StateFlow<LogStatus>
get() = _logStatus
suspend fun loadLog(git: Git) = withContext(Dispatchers.IO) {
_logStatus.value = LogStatus.Loading
val currentBranch = branchesManager.currentBranchRef(git)
suspend fun loadLog(git: Git, currentBranch: Ref?) = withContext(Dispatchers.IO) {
val commitList = GraphCommitList()
val repositoryState = git.repository.repositoryState
@ -45,7 +36,7 @@ class LogManager @Inject constructor(
walk.markStartAllRefs(Constants.R_REMOTES)
walk.markStartAllRefs(Constants.R_TAGS)
if (statusManager.checkHasUncommitedChanges(git))
if (statusManager.hasUncommitedChanges(git))
commitList.addUncommitedChangesGraphCommit(logList.first())
commitList.source(walk)
@ -55,9 +46,8 @@ class LogManager @Inject constructor(
ensureActive()
}
val loadedStatus = LogStatus.Loaded(commitList, currentBranch)
_logStatus.value = loadedStatus
return@withContext commitList
}
suspend fun checkoutCommit(git: Git, revCommit: RevCommit) = withContext(Dispatchers.IO) {
@ -67,19 +57,6 @@ class LogManager @Inject constructor(
.call()
}
suspend fun checkoutRef(git: Git, ref: Ref) = withContext(Dispatchers.IO) {
git.checkout().apply {
setName(ref.name)
if (ref.isBranch && ref.name.startsWith("refs/remotes/")) {
setCreateBranch(true)
setName(ref.simpleName)
setStartPoint(ref.objectId.name)
setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK)
}
call()
}
}
suspend fun revertCommit(git: Git, revCommit: RevCommit) = withContext(Dispatchers.IO) {
git
.revert()
@ -106,9 +83,4 @@ enum class ResetType {
SOFT,
MIXED,
HARD,
}
sealed class LogStatus {
object Loading : LogStatus()
class Loaded(val plotCommitList: GraphCommitList, val currentBranch: Ref?) : LogStatus()
}

View file

@ -10,22 +10,18 @@ import org.eclipse.jgit.transport.RemoteConfig
import javax.inject.Inject
class RemotesManager @Inject constructor() {
private val _remotes = MutableStateFlow<List<RemoteInfo>>(listOf())
val remotes: StateFlow<List<RemoteInfo>>
get() = _remotes
suspend fun loadRemotes(git: Git, allRemoteBranches: List<Ref>) = withContext(Dispatchers.IO) {
val remotes = git.remoteList()
.call()
val remoteInfoList = remotes.map { remoteConfig ->
return@withContext remotes.map { remoteConfig ->
val remoteBranches = allRemoteBranches.filter { branch ->
branch.name.startsWith("refs/remotes/${remoteConfig.name}")
}
RemoteInfo(remoteConfig, remoteBranches)
}
_remotes.value = remoteInfoList
}
}

View file

@ -0,0 +1,12 @@
package app.git
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import javax.inject.Inject
class RepositoryManager @Inject constructor() {
suspend fun getRepositoryState(git: Git) = withContext(Dispatchers.IO) {
return@withContext git.repository.repositoryState
}
}

View file

@ -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) {
@ -175,7 +86,7 @@ class StatusManager @Inject constructor(
completedWithErrors = false
loadStatus(git)
// loadStatus(git)
} finally {
if (completedWithErrors)
dirCache.unlock()
@ -226,7 +137,7 @@ class StatusManager @Inject constructor(
completedWithErrors = false
loadStatus(git)
// loadStatus(git)
} finally {
if (completedWithErrors)
dirCache.unlock()
@ -271,8 +182,6 @@ class StatusManager @Inject constructor(
git.reset()
.addPath(diffEntry.filePath)
.call()
loadStatus(git)
}
suspend fun commit(git: Git, message: String) = withContext(Dispatchers.IO) {
@ -280,8 +189,6 @@ class StatusManager @Inject constructor(
.setMessage(message)
.setAllowEmpty(false)
.call()
loadStatus(git)
}
suspend fun reset(git: Git, diffEntry: DiffEntry, staged: Boolean) = withContext(Dispatchers.IO) {
@ -297,15 +204,13 @@ class StatusManager @Inject constructor(
.addPath(diffEntry.filePath)
.call()
loadStatus(git)
// loadStatus(git)
}
suspend fun unstageAll(git: Git) = withContext(Dispatchers.IO) {
git
.reset()
.call()
loadStatus(git)
}
suspend fun stageAll(git: Git) = withContext(Dispatchers.IO) {
@ -313,15 +218,58 @@ class StatusManager @Inject constructor(
.add()
.addFilepattern(".")
.call()
}
loadStatus(git)
suspend fun getStaged(git: Git, currentBranch: Ref?, repositoryState: RepositoryState) = withContext(Dispatchers.IO) {
return@withContext git
.diff()
.setShowNameAndStatusOnly(true).apply {
if (currentBranch == null && !repositoryState.isMerging && !repositoryState.isRebasing)
setOldTree(EmptyTreeIterator()) // Required if the repository is empty
setCached(true)
}
.call()
// TODO: Grouping and fitlering allows us to remove duplicates when conflicts appear, requires more testing (what happens in windows? /dev/null is a unix thing)
// TODO: Test if we should group by old path or new path
.groupBy {
if(it.newPath != "/dev/null")
it.newPath
else
it.oldPath
}
.map {
val entries = it.value
val hasConflicts =
(entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing))
StatusEntry(entries.first(), isConflict = hasConflicts)
}
}
suspend fun getUnstaged(git: Git, repositoryState: RepositoryState) = withContext(Dispatchers.IO) {
return@withContext git
.diff()
.setShowNameAndStatusOnly(true)
.call()
.groupBy {
if(it.oldPath != "/dev/null")
it.oldPath
else
it.newPath
}
.map {
val entries = it.value
val hasConflicts =
(entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing))
StatusEntry(entries.first(), isConflict = hasConflicts)
}
}
}
sealed class StageStatus {
object Loading : StageStatus()
data class Loaded(val staged: List<StatusEntry>, val unstaged: List<StatusEntry>) : StageStatus()
}
data class StatusEntry(val diffEntry: DiffEntry, val isConflict: Boolean) {
val icon: ImageVector

View file

@ -0,0 +1,100 @@
package app.git
import app.app.Error
import app.app.newErrorNow
import app.di.TabScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.eclipse.jgit.api.Git
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException
@TabScope
class TabState @Inject constructor() {
var git: Git? = null
val safeGit: Git
get() {
val git = this.git
if (git == null) {
// _repositorySelectionStatus.value = RepositorySelectionStatus.None
throw CancellationException("Null git object")
} else
return git
}
val mutex = Mutex()
private val _refreshData = MutableSharedFlow<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>
get() = _processing
fun safeProcessing(showError: Boolean = true, callback: suspend (git: Git) -> RefreshType) =
managerScope.launch {
mutex.withLock {
_processing.value = true
operationRunning = true
try {
val refreshType = callback(safeGit)
if (refreshType != RefreshType.NONE)
_refreshData.emit(refreshType)
} catch (ex: Exception) {
ex.printStackTrace()
if (showError)
_errors.emit(newErrorNow(ex, ex.localizedMessage))
} finally {
_processing.value = false
operationRunning = false
}
}
}
fun runOperation(block: suspend (git: Git) -> RefreshType) = managerScope.launch {
operationRunning = true
try {
val refreshType = block(safeGit)
if (refreshType != RefreshType.NONE)
_refreshData.emit(refreshType)
} finally {
operationRunning = false
}
}
}
enum class RefreshType {
NONE,
ALL_DATA,
ONLY_LOG,
/**
* Requires to update the status if currently selected and update the log if there has been a change
* in the "uncommited changes" state (if there were changes before but not anymore and vice-versa)
*/
UNCOMMITED_CHANGES,
}

View file

@ -10,16 +10,8 @@ import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
class TagsManager @Inject constructor() {
private val _tags = MutableStateFlow<List<Ref>>(listOf())
val tags: StateFlow<List<Ref>>
get() = _tags
suspend fun loadTags(git: Git) = withContext(Dispatchers.IO) {
val branchList = git.tagList().call()
_tags.value = branchList
suspend fun getTags(git: Git) = withContext(Dispatchers.IO) {
return@withContext git.tagList().call()
}
suspend fun createTagOnCommit(git: Git, tag: String, revCommit: RevCommit) = withContext(Dispatchers.IO) {

View file

@ -21,7 +21,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.LoadingRepository
import app.credentials.CredentialsState
import app.git.GitManager
import app.git.TabViewModel
import app.git.RepositorySelectionStatus
import app.ui.dialogs.PasswordDialog
import app.ui.dialogs.UserPasswordDialog
@ -30,7 +30,7 @@ import kotlinx.coroutines.delay
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AppTab(
gitManager: GitManager,
gitManager: TabViewModel,
repositoryPath: String?,
tabName: MutableState<String>
) {
@ -101,7 +101,7 @@ fun AppTab(
LoadingRepository()
}
is RepositorySelectionStatus.Open -> {
RepositoryOpenPage(gitManager = gitManager)
RepositoryOpenPage(tabViewModel = gitManager)
}
}
}
@ -158,7 +158,7 @@ fun AppTab(
}
@Composable
fun CredentialsDialog(gitManager: GitManager) {
fun CredentialsDialog(gitManager: TabViewModel) {
val credentialsState by gitManager.credentialsState.collectAsState()
if (credentialsState == CredentialsState.HttpCredentialsRequested) {

View file

@ -13,23 +13,23 @@ import androidx.compose.ui.unit.dp
import app.MAX_SIDE_PANEL_ITEMS_HEIGHT
import app.extensions.isLocal
import app.extensions.simpleName
import app.git.GitManager
import app.git.TabViewModel
import app.ui.components.ScrollableLazyColumn
import app.ui.components.SideMenuEntry
import app.ui.components.SideMenuSubentry
import app.ui.components.entryHeight
import app.ui.context_menu.branchContextMenuItems
import app.ui.dialogs.MergeDialog
import app.viewmodels.BranchesViewModel
import org.eclipse.jgit.lib.Ref
@Composable
fun Branches(
gitManager: GitManager,
branchesViewModel: BranchesViewModel,
onBranchClicked: (Ref) -> Unit,
) {
val branches by gitManager.branches.collectAsState()
val currentBranch by gitManager.currentBranch.collectAsState()
) {
val branches by branchesViewModel.branches.collectAsState()
val currentBranch by branchesViewModel.currentBranch.collectAsState()
val (mergeBranch, setMergeBranch) = remember { mutableStateOf<Ref?>(null) }
Column {
@ -48,9 +48,9 @@ fun Branches(
branch = branch,
isCurrentBranch = currentBranch == branch.name,
onBranchClicked = { onBranchClicked(branch) },
onCheckoutBranch = { gitManager.checkoutRef(branch) },
onCheckoutBranch = { branchesViewModel.checkoutRef(branch) },
onMergeBranch = { setMergeBranch(branch) },
onDeleteBranch = { gitManager.deleteBranch(branch) },
onDeleteBranch = { branchesViewModel.deleteBranch(branch) },
)
}
}
@ -62,7 +62,7 @@ fun Branches(
currentBranch,
mergeBranchName = mergeBranch.name,
onReject = { setMergeBranch(null) },
onAccept = { ff -> gitManager.mergeBranch(mergeBranch, ff) }
onAccept = { ff -> branchesViewModel.mergeBranch(mergeBranch, ff) }
)
}
}

View file

@ -3,7 +3,6 @@ package app.ui
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Divider
import androidx.compose.material.Icon
@ -12,14 +11,13 @@ import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.extensions.*
import app.git.GitManager
import app.git.TabViewModel
import app.theme.headerBackground
import app.theme.headerText
import app.theme.primaryTextColor
@ -32,7 +30,7 @@ import org.eclipse.jgit.revwalk.RevCommit
@Composable
fun CommitChanges(
gitManager: GitManager,
gitManager: TabViewModel,
commit: RevCommit,
onDiffSelected: (DiffEntry) -> Unit
) {

View file

@ -16,24 +16,25 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.git.DiffEntryType
import app.git.GitManager
import app.git.TabViewModel
import app.git.diff.Hunk
import app.git.diff.LineType
import app.theme.primaryTextColor
import app.ui.components.ScrollableLazyColumn
import app.ui.components.SecondaryButton
import app.viewmodels.DiffViewModel
import org.eclipse.jgit.diff.DiffEntry
@Composable
fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView: () -> Unit) {
var text by remember { mutableStateOf(listOf<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
@ -59,7 +60,7 @@ fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView:
.fillMaxSize()
// .padding(16.dp)
) {
itemsIndexed(text) { index, hunk ->
itemsIndexed(hunks) { index, hunk ->
val hunksSeparation = if (index == 0)
0.dp
else
@ -96,9 +97,9 @@ fun Diff(gitManager: GitManager, diffEntryType: DiffEntryType, onCloseDiffView:
backgroundButton = color,
onClick = {
if (diffEntryType is DiffEntryType.StagedDiff) {
gitManager.unstageHunk(diffEntryType.diffEntry, hunk)
diffViewModel.unstageHunk(diffEntryType.diffEntry, hunk)
} else {
gitManager.stageHunk(diffEntryType.diffEntry, hunk)
diffViewModel.stageHunk(diffEntryType.diffEntry, hunk)
}
}
)

View file

@ -12,16 +12,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import app.MAX_SIDE_PANEL_ITEMS_HEIGHT
import app.extensions.simpleVisibleName
import app.git.GitManager
import app.git.TabViewModel
import app.git.RemoteInfo
import app.ui.components.ScrollableLazyColumn
import app.ui.components.SideMenuEntry
import app.ui.components.SideMenuSubentry
import app.ui.components.entryHeight
import app.viewmodels.RemotesViewModel
@Composable
fun Remotes(gitManager: GitManager) {
val remotes by gitManager.remotes.collectAsState()
fun Remotes(remotesViewModel: RemotesViewModel) {
val remotes by remotesViewModel.remotes.collectAsState()
Column {
SideMenuEntry("Remotes")

View file

@ -6,7 +6,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import app.git.DiffEntryType
import app.git.GitManager
import app.git.TabViewModel
import app.ui.dialogs.NewBranchDialog
import app.ui.log.Log
import openRepositoryDialog
@ -18,15 +18,22 @@ import org.jetbrains.compose.splitpane.rememberSplitPaneState
@OptIn(ExperimentalSplitPaneApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
@Composable
fun RepositoryOpenPage(gitManager: GitManager) {
fun RepositoryOpenPage(tabViewModel: TabViewModel) {
val repositoryState by tabViewModel.repositoryState.collectAsState()
var diffSelected by remember {
mutableStateOf<DiffEntryType?>(null)
}
LaunchedEffect(diffSelected) {
diffSelected?.let { safeDiffSelected ->
tabViewModel.updatedDiffEntry(safeDiffSelected)
}
}
var showNewBranchDialog by remember { mutableStateOf(false) }
val (selectedItem, setSelectedItem) = remember { mutableStateOf<SelectedItem>(SelectedItem.None) }
LaunchedEffect(selectedItem) {
diffSelected = null
}
@ -37,7 +44,7 @@ fun RepositoryOpenPage(gitManager: GitManager) {
showNewBranchDialog = false
},
onAccept = { branchName ->
gitManager.createBranch(branchName)
tabViewModel.branchesViewModel.createBranch(branchName)
showNewBranchDialog = false
}
)
@ -46,12 +53,12 @@ fun RepositoryOpenPage(gitManager: GitManager) {
Column {
GMenu(
onRepositoryOpen = {
openRepositoryDialog(gitManager = gitManager)
openRepositoryDialog(gitManager = tabViewModel)
},
onPull = { gitManager.pull() },
onPush = { gitManager.push() },
onStash = { gitManager.stash() },
onPopStash = { gitManager.popStash() },
onPull = { tabViewModel.pull() },
onPush = { tabViewModel.push() },
onStash = { tabViewModel.stash() },
onPopStash = { tabViewModel.popStash() },
onCreateBranch = { showNewBranchDialog = true }
)
@ -65,22 +72,22 @@ fun RepositoryOpenPage(gitManager: GitManager) {
.fillMaxHeight()
) {
Branches(
gitManager = gitManager,
branchesViewModel = tabViewModel.branchesViewModel,
onBranchClicked = {
val commit = gitManager.findCommit(it.objectId)
val commit = tabViewModel.findCommit(it.objectId)
setSelectedItem(SelectedItem.Ref(commit))
}
)
Remotes(gitManager = gitManager)
Remotes(remotesViewModel = tabViewModel.remotesViewModel)
Tags(
gitManager = gitManager,
tagsViewModel = tabViewModel.tagsViewModel,
onTagClicked = {
val commit = gitManager.findCommit(it.objectId)
val commit = tabViewModel.findCommit(it.objectId)
setSelectedItem(SelectedItem.Ref(commit))
}
)
Stashes(
gitManager = gitManager,
gitManager = tabViewModel,
onStashSelected = { stash ->
setSelectedItem(SelectedItem.Stash(stash))
}
@ -97,11 +104,13 @@ fun RepositoryOpenPage(gitManager: GitManager) {
modifier = Modifier
.fillMaxSize()
) {
Crossfade(targetState = diffSelected) { diffEntry ->
when (diffEntry) {
// Crossfade(targetState = diffSelected) { diffEntry ->
when (diffSelected) {
null -> {
Log(
gitManager = gitManager,
tabViewModel = tabViewModel,
repositoryState = repositoryState,
logViewModel = tabViewModel.logViewModel,
selectedItem = selectedItem,
onItemSelected = {
setSelectedItem(it)
@ -110,12 +119,11 @@ fun RepositoryOpenPage(gitManager: GitManager) {
}
else -> {
Diff(
gitManager = gitManager,
diffEntryType = diffEntry,
diffViewModel = tabViewModel.diffViewModel,
onCloseDiffView = { diffSelected = null })
}
}
}
// }
}
}
@ -126,8 +134,9 @@ fun RepositoryOpenPage(gitManager: GitManager) {
) {
if (selectedItem == SelectedItem.UncommitedChanges) {
UncommitedChanges(
gitManager = gitManager,
statusViewModel = tabViewModel.statusViewModel,
selectedEntryType = diffSelected,
repositoryState = repositoryState,
onStagedDiffEntrySelected = { diffEntry ->
diffSelected = if (diffEntry != null)
DiffEntryType.StagedDiff(diffEntry)
@ -140,7 +149,7 @@ fun RepositoryOpenPage(gitManager: GitManager) {
)
} else if (selectedItem is SelectedItem.CommitBasedItem) {
CommitChanges(
gitManager = gitManager,
gitManager = tabViewModel,
commit = selectedItem.revCommit,
onDiffSelected = { diffEntry ->
diffSelected = DiffEntryType.CommitDiff(diffEntry)

View file

@ -6,7 +6,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import app.git.GitManager
import app.git.TabViewModel
import app.git.StashStatus
import app.ui.components.ScrollableLazyColumn
import app.ui.components.SideMenuEntry
@ -15,7 +15,7 @@ import org.eclipse.jgit.revwalk.RevCommit
@Composable
fun Stashes(
gitManager: GitManager,
gitManager: TabViewModel,
onStashSelected: (commit: RevCommit) -> Unit,
) {
val stashStatusState = gitManager.stashStatus.collectAsState()

View file

@ -1,9 +1,9 @@
import app.extensions.runCommand
import app.git.GitManager
import app.git.TabViewModel
import javax.swing.JFileChooser
fun openRepositoryDialog(gitManager: GitManager) {
fun openRepositoryDialog(gitManager: TabViewModel) {
val os = System.getProperty("os.name")
val appStateManager = gitManager.appStateManager
val latestDirectoryOpened = appStateManager.latestOpenedRepositoryPath
@ -29,7 +29,7 @@ fun openRepositoryDialog(gitManager: GitManager) {
}
private fun openRepositoryDialog(
gitManager: GitManager,
gitManager: TabViewModel,
latestDirectoryOpened: String
) {

View file

@ -13,20 +13,21 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import app.MAX_SIDE_PANEL_ITEMS_HEIGHT
import app.extensions.simpleName
import app.git.GitManager
import app.git.TabViewModel
import app.ui.components.ScrollableLazyColumn
import app.ui.components.SideMenuEntry
import app.ui.components.SideMenuSubentry
import app.ui.components.entryHeight
import app.ui.context_menu.tagContextMenuItems
import app.viewmodels.TagsViewModel
import org.eclipse.jgit.lib.Ref
@Composable
fun Tags(
gitManager: GitManager,
tagsViewModel: TagsViewModel,
onTagClicked: (Ref) -> Unit,
) {
val tagsState = gitManager.tags.collectAsState()
val tagsState = tagsViewModel.tags.collectAsState()
val tags = tagsState.value
Column {
@ -46,8 +47,8 @@ fun Tags(
TagRow(
tag = tag,
onTagClicked = { onTagClicked(tag) },
onCheckoutTag = { gitManager.checkoutRef(tag) },
onDeleteTag = { gitManager.deleteTag(tag) }
onCheckoutTag = { tagsViewModel.checkoutRef(tag) },
onDeleteTag = { tagsViewModel.deleteTag(tag) }
)
}
}

View file

@ -28,36 +28,30 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.extensions.filePath
import app.extensions.icon
import app.extensions.iconColor
import app.extensions.isMerging
import app.git.DiffEntryType
import app.git.GitManager
import app.git.StageStatus
import app.git.StatusEntry
import app.theme.headerBackground
import app.theme.headerText
import app.theme.primaryTextColor
import app.ui.components.ScrollableLazyColumn
import app.ui.components.SecondaryButton
import app.viewmodels.StageStatus
import app.viewmodels.StatusViewModel
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.lib.RepositoryState
@OptIn(ExperimentalAnimationApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
@Composable
fun UncommitedChanges(
gitManager: GitManager,
statusViewModel: StatusViewModel,
selectedEntryType: DiffEntryType?,
repositoryState: RepositoryState,
onStagedDiffEntrySelected: (DiffEntry?) -> Unit,
onUnstagedDiffEntrySelected: (DiffEntry) -> Unit,
) {
val stageStatusState = gitManager.stageStatus.collectAsState()
val stageStatusState = statusViewModel.stageStatus.collectAsState()
val stageStatus = stageStatusState.value
val lastCheck by gitManager.lastTimeChecked.collectAsState()
val repositoryState by gitManager.repositoryState.collectAsState()
LaunchedEffect(lastCheck) {
gitManager.loadStatus()
}
val staged: List<StatusEntry>
val unstaged: List<StatusEntry>
@ -83,7 +77,7 @@ fun UncommitedChanges(
var commitMessage by remember { mutableStateOf("") }
val doCommit = {
gitManager.commit(commitMessage)
statusViewModel.commit(commitMessage)
onStagedDiffEntrySelected(null)
commitMessage = ""
}
@ -111,13 +105,13 @@ fun UncommitedChanges(
diffEntries = staged,
onDiffEntrySelected = onStagedDiffEntrySelected,
onDiffEntryOptionSelected = {
gitManager.unstage(it)
statusViewModel.unstage(it)
},
onReset = { diffEntry ->
gitManager.resetStaged(diffEntry)
statusViewModel.resetStaged(diffEntry)
},
onAllAction = {
gitManager.unstageAll()
statusViewModel.unstageAll()
}
)
@ -132,13 +126,13 @@ fun UncommitedChanges(
diffEntries = unstaged,
onDiffEntrySelected = onUnstagedDiffEntrySelected,
onDiffEntryOptionSelected = {
gitManager.stage(it)
statusViewModel.stage(it)
},
onReset = { diffEntry ->
gitManager.resetUnstaged(diffEntry)
statusViewModel.resetUnstaged(diffEntry)
},
{
gitManager.stageAll()
statusViewModel.stageAll()
},
allActionTitle = "Stage all"
)

View file

@ -20,7 +20,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.extensions.dirName
import app.extensions.dirPath
import app.git.GitManager
import app.git.TabViewModel
import app.theme.primaryTextColor
import app.theme.secondaryTextColor
import app.ui.dialogs.CloneDialog
@ -33,7 +33,7 @@ import java.net.URI
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun WelcomePage(
gitManager: GitManager,
gitManager: TabViewModel,
) {
val appStateManager = gitManager.appStateManager
var showCloneView by remember { mutableStateOf(false) }

View file

@ -20,8 +20,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import app.AppStateManager
import app.di.AppComponent
import app.di.DaggerTabComponent
import app.git.TabViewModel
import app.theme.tabColorActive
import app.theme.tabColorInactive
import app.ui.AppTab
import javax.inject.Inject
@Composable
@ -48,7 +54,7 @@ fun RepositoriesTabPanel(
) {
items(items = tabs) { tab ->
Tab(
title = tab.title,
title = tab.tabName,
selected = tab.key == selectedTabKey,
onClick = {
onTabSelected(tab.key)
@ -154,7 +160,33 @@ fun Tab(title: MutableState<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 gitManager: TabViewModel
@Inject
lateinit var appStateManager: AppStateManager
val content: @Composable (TabInformation) -> Unit
)
init {
val tabComponent = DaggerTabComponent.builder()
.appComponent(appComponent)
.build()
tabComponent.inject(this)
gitManager.onRepositoryChanged = { path ->
if (path == null) {
appStateManager.repositoryTabRemoved(key)
} else
appStateManager.repositoryTabChanged(key, path)
}
content = {
AppTab(gitManager, path, tabName)
}
}
}

View file

@ -12,13 +12,13 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.git.CloneStatus
import app.git.GitManager
import app.git.TabViewModel
import app.theme.primaryTextColor
import java.io.File
@Composable
fun CloneDialog(
gitManager: GitManager,
gitManager: TabViewModel,
onClose: () -> Unit
) {
val cloneStatus = gitManager.cloneStatus.collectAsState()

View file

@ -34,8 +34,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.extensions.*
import app.git.GitManager
import app.git.LogStatus
import app.git.TabViewModel
import app.git.graph.GraphNode
import app.theme.*
import app.ui.SelectedItem
@ -47,6 +46,8 @@ import app.ui.dialogs.MergeDialog
import app.ui.dialogs.NewBranchDialog
import app.ui.dialogs.NewTagDialog
import app.ui.dialogs.ResetBranchDialog
import app.viewmodels.LogStatus
import app.viewmodels.LogViewModel
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.lib.RepositoryState
import org.eclipse.jgit.revwalk.RevCommit
@ -70,13 +71,14 @@ private const val CANVAS_MIN_WIDTH = 100
)
@Composable
fun Log(
gitManager: GitManager,
tabViewModel: TabViewModel,
logViewModel: LogViewModel,
selectedItem: SelectedItem,
onItemSelected: (SelectedItem) -> Unit,
repositoryState: RepositoryState,
) {
val logStatusState = gitManager.logStatus.collectAsState()
val logStatusState = logViewModel.logStatus.collectAsState()
val logStatus = logStatusState.value
val repositoryState by gitManager.repositoryState.collectAsState()
val showLogDialog = remember { mutableStateOf<LogDialog>(LogDialog.None) }
val selectedCommit = if (selectedItem is SelectedItem.CommitBasedItem) {
@ -86,6 +88,7 @@ fun Log(
}
if (logStatus is LogStatus.Loaded) {
val hasUncommitedChanges = logStatus.hasUncommitedChanges
val commitList = logStatus.plotCommitList
val scrollState = rememberLazyListState()
@ -102,7 +105,7 @@ fun Log(
}
LogDialogs(
gitManager,
logViewModel,
currentBranch = logStatus.currentBranch,
onResetShowLogDialog = { showLogDialog.value = LogDialog.None },
showLogDialog = showLogDialog.value,
@ -114,7 +117,7 @@ fun Log(
.background(MaterialTheme.colors.background)
.fillMaxSize()
) {
val hasUncommitedChanges by gitManager.hasUncommitedChanges.collectAsState()
// val hasUncommitedChanges by tabViewModel.hasUncommitedChanges.collectAsState()
val weightMod = remember { mutableStateOf(0f) }
var graphWidth = (CANVAS_MIN_WIDTH + weightMod.value).dp
@ -131,11 +134,12 @@ fun Log(
.background(MaterialTheme.colors.background)
.fillMaxSize(),
) {
//TODO: Shouldn't this be an item of the graph?
if (hasUncommitedChanges)
item {
UncommitedChangesLine(
selected = selectedItem == SelectedItem.UncommitedChanges,
hasPreviousCommits = commitList.count() > 0,
hasPreviousCommits = commitList.isNotEmpty(),
graphWidth = graphWidth,
weightMod = weightMod,
repositoryState = repositoryState,
@ -146,7 +150,7 @@ fun Log(
}
items(items = commitList) { graphNode ->
CommitLine(
gitManager = gitManager,
logViewModel = logViewModel,
graphNode = graphNode,
selected = selectedCommit?.name == graphNode.name,
weightMod = weightMod,
@ -169,7 +173,7 @@ fun Log(
@Composable
fun LogDialogs(
gitManager: GitManager,
logViewModel: LogViewModel,
onResetShowLogDialog: () -> Unit,
showLogDialog: LogDialog,
currentBranch: Ref?,
@ -179,7 +183,7 @@ fun LogDialogs(
NewBranchDialog(
onReject = onResetShowLogDialog,
onAccept = { branchName ->
gitManager.createBranchOnCommit(branchName, showLogDialog.graphNode)
logViewModel.createBranchOnCommit(branchName, showLogDialog.graphNode)
onResetShowLogDialog()
}
)
@ -188,7 +192,7 @@ fun LogDialogs(
NewTagDialog(
onReject = onResetShowLogDialog,
onAccept = { tagName ->
gitManager.createTagOnCommit(tagName, showLogDialog.graphNode)
logViewModel.createTagOnCommit(tagName, showLogDialog.graphNode)
onResetShowLogDialog()
}
)
@ -200,7 +204,7 @@ fun LogDialogs(
mergeBranchName = showLogDialog.ref.simpleName,
onReject = onResetShowLogDialog,
onAccept = { ff ->
gitManager.mergeBranch(showLogDialog.ref, ff)
logViewModel.mergeBranch(showLogDialog.ref, ff)
onResetShowLogDialog()
}
)
@ -208,7 +212,7 @@ fun LogDialogs(
is LogDialog.ResetBranch -> ResetBranchDialog(
onReject = onResetShowLogDialog,
onAccept = { resetType ->
gitManager.resetToCommit(showLogDialog.graphNode, resetType)
logViewModel.resetToCommit(showLogDialog.graphNode, resetType)
onResetShowLogDialog()
}
)
@ -324,7 +328,7 @@ fun UncommitedChangesLine(
@Composable
fun CommitLine(
gitManager: GitManager,
logViewModel: LogViewModel,
graphNode: GraphNode,
selected: Boolean,
weightMod: MutableState<Float>,
@ -348,9 +352,7 @@ fun CommitLine(
listOf(
ContextMenuItem(
label = "Checkout commit",
onClick = {
gitManager.checkoutCommit(graphNode)
}),
onClick = { logViewModel.checkoutCommit(graphNode) }),
ContextMenuItem(
label = "Create branch",
onClick = showCreateNewBranch
@ -361,7 +363,7 @@ fun CommitLine(
),
ContextMenuItem(
label = "Revert commit",
onClick = { gitManager.revertCommit(graphNode) }
onClick = { logViewModel.revertCommit(graphNode) }
),
ContextMenuItem(
@ -403,10 +405,10 @@ fun CommitLine(
refs = commitRefs,
nodeColor = nodeColor,
currentBranch = currentBranch,
onCheckoutRef = { ref -> gitManager.checkoutRef(ref) },
onCheckoutRef = { ref -> logViewModel.checkoutRef(ref) },
onMergeBranch = { ref -> onMergeBranch(ref) },
onDeleteBranch = { ref -> gitManager.deleteBranch(ref) },
onDeleteTag = { ref -> gitManager.deleteTag(ref) },
onDeleteBranch = { ref -> logViewModel.deleteBranch(ref) },
onDeleteTag = { ref -> logViewModel.deleteTag(ref) },
)
}
}

View file

@ -0,0 +1,60 @@
package app.viewmodels
import app.git.BranchesManager
import app.git.RefreshType
import app.git.TabState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
class BranchesViewModel @Inject constructor(
private val branchesManager: BranchesManager,
private val tabState: TabState,
) {
private val _branches = MutableStateFlow<List<Ref>>(listOf())
val branches: StateFlow<List<Ref>>
get() = _branches
private val _currentBranch = MutableStateFlow<String>("")
val currentBranch: StateFlow<String>
get() = _currentBranch
suspend fun loadBranches(git: Git) {
val branchesList = branchesManager.getBranches(git)
_branches.value = branchesList
_currentBranch.value = branchesManager.currentBranchRef(git)?.name ?: ""
}
fun createBranch(branchName: String) = tabState.safeProcessing { git ->
branchesManager.createBranch(git, branchName)
this.loadBranches(git)
return@safeProcessing RefreshType.NONE
}
fun mergeBranch(ref: Ref, fastForward: Boolean) = tabState.safeProcessing { git ->
branchesManager.mergeBranch(git, ref, fastForward)
return@safeProcessing RefreshType.ALL_DATA
}
fun deleteBranch(branch: Ref) =tabState.safeProcessing { git ->
branchesManager.deleteBranch(git, branch)
return@safeProcessing RefreshType.ALL_DATA
}
fun checkoutRef(ref: Ref) = tabState.safeProcessing { git ->
branchesManager.checkoutRef(git, ref)
return@safeProcessing RefreshType.ALL_DATA
}
suspend fun refresh(git: Git) {
loadBranches(git)
}
}

View file

@ -0,0 +1,44 @@
package app.viewmodels
import app.git.*
import app.git.diff.Hunk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.diff.DiffEntry
import javax.inject.Inject
class DiffViewModel @Inject constructor(
private val tabState: TabState,
private val diffManager: DiffManager,
private val statusManager: StatusManager,
) {
// TODO Maybe use a sealed class instead of a null to represent that a diff is not selected?
private val _diffResult = MutableStateFlow<DiffResult?>(null)
val diffResult: StateFlow<DiffResult?> = _diffResult
suspend fun updateDiff(git: Git, diffEntryType: DiffEntryType) = withContext(Dispatchers.IO) {
_diffResult.value = null
val hunks = diffManager.diffFormat(git, diffEntryType)
_diffResult.value = DiffResult(diffEntryType, hunks)
}
fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation { git ->
statusManager.stageHunk(git, diffEntry, hunk)
return@runOperation RefreshType.UNCOMMITED_CHANGES
}
fun unstageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation { git ->
statusManager.unstageHunk(git, diffEntry, hunk)
return@runOperation RefreshType.UNCOMMITED_CHANGES
}
}
data class DiffResult(val diffEntryType: DiffEntryType, val hunks: List<Hunk>)

View file

@ -0,0 +1,97 @@
package app.viewmodels
import app.git.*
import app.git.graph.GraphCommitList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
class LogViewModel @Inject constructor(
private val logManager: LogManager,
private val statusManager: StatusManager,
private val branchesManager: BranchesManager,
private val tagsManager: TagsManager,
private val tabState: TabState,
) {
private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading)
val logStatus: StateFlow<LogStatus>
get() = _logStatus
suspend fun loadLog(git: Git) {
_logStatus.value = LogStatus.Loading
val currentBranch = branchesManager.currentBranchRef(git)
val log = logManager.loadLog(git, currentBranch)
val hasUncommitedChanges = statusManager.hasUncommitedChanges(git)
_logStatus.value = LogStatus.Loaded(hasUncommitedChanges, log, currentBranch)
}
fun checkoutCommit(revCommit: RevCommit) = tabState.safeProcessing { git ->
logManager.checkoutCommit(git, revCommit)
return@safeProcessing RefreshType.ALL_DATA
}
fun revertCommit(revCommit: RevCommit) = tabState.safeProcessing { git ->
logManager.revertCommit(git, revCommit)
return@safeProcessing RefreshType.ALL_DATA
}
fun resetToCommit(revCommit: RevCommit, resetType: ResetType) = tabState.safeProcessing { git ->
logManager.resetToCommit(git, revCommit, resetType = resetType)
return@safeProcessing RefreshType.ALL_DATA
}
fun checkoutRef(ref: Ref) = tabState.safeProcessing { git ->
branchesManager.checkoutRef(git, ref)
return@safeProcessing RefreshType.ALL_DATA
}
fun createBranchOnCommit(branch: String, revCommit: RevCommit) = tabState.safeProcessing { git ->
branchesManager.createBranchOnCommit(git, branch, revCommit)
return@safeProcessing RefreshType.ALL_DATA
}
fun createTagOnCommit(tag: String, revCommit: RevCommit) = tabState.safeProcessing { git ->
tagsManager.createTagOnCommit(git, tag, revCommit)
return@safeProcessing RefreshType.ALL_DATA
}
fun mergeBranch(ref: Ref, fastForward: Boolean) = tabState.safeProcessing { git ->
branchesManager.mergeBranch(git, ref, fastForward)
return@safeProcessing RefreshType.ALL_DATA
}
fun deleteBranch(branch: Ref) =tabState.safeProcessing { git ->
branchesManager.deleteBranch(git, branch)
return@safeProcessing RefreshType.ALL_DATA
}
fun deleteTag(tag: Ref) = tabState.safeProcessing { git ->
tagsManager.deleteTag(git, tag)
return@safeProcessing RefreshType.ALL_DATA
}
suspend fun refresh(git: Git) {
loadLog(git)
}
}
sealed class LogStatus {
object Loading : LogStatus()
class Loaded(val hasUncommitedChanges: Boolean, val plotCommitList: GraphCommitList, val currentBranch: Ref?) : LogStatus()
}

View file

@ -0,0 +1,44 @@
package app.viewmodels
import app.git.BranchesManager
import app.git.RemoteInfo
import app.git.RemotesManager
import app.git.TabState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.transport.RemoteConfig
import javax.inject.Inject
class RemotesViewModel @Inject constructor(
private val remotesManager: RemotesManager,
private val branchesManager: BranchesManager,
) {
private val _remotes = MutableStateFlow<List<RemoteInfo>>(listOf())
val remotes: StateFlow<List<RemoteInfo>>
get() = _remotes
suspend fun loadRemotes(git: Git) = withContext(Dispatchers.IO) {
val remotes = git.remoteList()
.call()
val allRemoteBranches = branchesManager.remoteBranches(git)
remotesManager.loadRemotes(git, allRemoteBranches)
val remoteInfoList = remotes.map { remoteConfig ->
val remoteBranches = allRemoteBranches.filter { branch ->
branch.name.startsWith("refs/remotes/${remoteConfig.name}")
}
RemoteInfo(remoteConfig, remoteBranches)
}
_remotes.value = remoteInfoList
}
suspend fun refresh(git: Git) = withContext(Dispatchers.IO) {
loadRemotes(git)
}
}

View file

@ -0,0 +1,124 @@
package app.viewmodels
import app.git.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.diff.DiffEntry
import javax.inject.Inject
class StatusViewModel @Inject constructor(
private val tabState: TabState,
private val statusManager: StatusManager,
private val branchesManager: BranchesManager,
private val repositoryManager: RepositoryManager,
) {
private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf()))
val stageStatus: StateFlow<StageStatus> = _stageStatus
private val _hasUncommitedChanges = MutableStateFlow<Boolean>(false)
val hasUncommitedChanges: StateFlow<Boolean>
get() = _hasUncommitedChanges
fun stage(diffEntry: DiffEntry) = tabState.runOperation { git ->
statusManager.stage(git, diffEntry)
loadStatus(git)
return@runOperation RefreshType.NONE
}
fun unstage(diffEntry: DiffEntry) = tabState.runOperation { git ->
statusManager.unstage(git, diffEntry)
loadStatus(git)
return@runOperation RefreshType.NONE
}
fun unstageAll() = tabState.safeProcessing { git ->
statusManager.unstageAll(git)
loadStatus(git)
return@safeProcessing RefreshType.NONE
}
fun stageAll() = tabState.safeProcessing { git ->
statusManager.stageAll(git)
loadStatus(git)
return@safeProcessing RefreshType.NONE
}
fun resetStaged(diffEntry: DiffEntry) = tabState.runOperation { git ->
statusManager.reset(git, diffEntry, staged = true)
return@runOperation RefreshType.UNCOMMITED_CHANGES
}
fun resetUnstaged(diffEntry: DiffEntry) =tabState.runOperation { git ->
statusManager.reset(git, diffEntry, staged = false)
return@runOperation RefreshType.UNCOMMITED_CHANGES
}
suspend fun loadStatus(git: Git) {
val previousStatus = _stageStatus.value
try {
_stageStatus.value = StageStatus.Loading
val repositoryState = repositoryManager.getRepositoryState(git)
val currentBranchRef = branchesManager.currentBranchRef(git)
val staged = statusManager.getStaged(git, currentBranchRef, repositoryState)
val unstaged = statusManager.getUnstaged(git, repositoryState)
_stageStatus.value = StageStatus.Loaded(staged, unstaged)
} catch (ex: Exception) {
_stageStatus.value = previousStatus
throw ex
}
}
suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) {
_hasUncommitedChanges.value = statusManager.hasUncommitedChanges(git)
}
fun commit(message: String) = tabState.safeProcessing { git ->
statusManager.commit(git, message)
return@safeProcessing RefreshType.ALL_DATA
}
suspend fun refresh(git: Git) = withContext(Dispatchers.IO) {
loadStatus(git)
loadHasUncommitedChanges(git)
}
/**
* Checks if there are uncommited changes and returns if the state has changed (
*/
suspend fun updateHasUncommitedChanges(git: Git): Boolean {
val hadUncommitedChanges = hasUncommitedChanges.value
loadStatus(git)
val hasNowUncommitedChanges = hasUncommitedChanges.value
// Return true to update the log only if the uncommitedChanges status has changed
return (hasNowUncommitedChanges != hadUncommitedChanges)
}
}
sealed class StageStatus {
object Loading : StageStatus()
data class Loaded(val staged: List<StatusEntry>, val unstaged: List<StatusEntry>) : StageStatus()
}

View file

@ -6,6 +6,7 @@ import app.app.newErrorNow
import app.credentials.CredentialsState
import app.credentials.CredentialsStateManager
import app.git.diff.Hunk
import app.viewmodels.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -13,7 +14,6 @@ import kotlinx.coroutines.flow.collect
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.lib.RepositoryState
import org.eclipse.jgit.revwalk.RevCommit
@ -22,15 +22,18 @@ import java.io.File
import javax.inject.Inject
class GitManager @Inject constructor(
private val statusManager: StatusManager,
private val logManager: LogManager,
class TabViewModel @Inject constructor(
val logViewModel: LogViewModel,
val branchesViewModel: BranchesViewModel,
val tagsViewModel: TagsViewModel,
val remotesViewModel: RemotesViewModel,
val statusViewModel: StatusViewModel,
val diffViewModel: DiffViewModel,
private val repositoryManager: RepositoryManager,
private val remoteOperationsManager: RemoteOperationsManager,
private val branchesManager: BranchesManager,
private val stashManager: StashManager,
private val diffManager: DiffManager,
private val tagsManager: TagsManager,
private val remotesManager: RemotesManager,
private val tabState: TabState,
val errorsManager: ErrorsManager,
val appStateManager: AppStateManager,
private val fileChangesWatcher: FileChangesWatcher,
@ -50,22 +53,31 @@ class GitManager @Inject constructor(
val processing: StateFlow<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
private val _repositoryState = MutableStateFlow(RepositoryState.SAFE)
val repositoryState: StateFlow<RepositoryState> = _repositoryState
init {
managerScope.launch {
tabState.refreshData.collect { refreshType ->
when (refreshType) {
RefreshType.NONE -> println("Not refreshing...")
RefreshType.ALL_DATA -> refreshRepositoryInfo()
RefreshType.ONLY_LOG -> refreshLog()
RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges()
}
}
}
}
private fun refreshLog() = tabState.runOperation { git ->
logViewModel.refresh(git)
return@runOperation RefreshType.NONE
}
/**
* Property that indicates if a git operation is running
@ -74,7 +86,7 @@ class GitManager @Inject constructor(
private val safeGit: Git
get() {
val git = this.git
val git = this.tabState.git
if (git == null) {
_repositorySelectionStatus.value = RepositorySelectionStatus.None
throw CancellationException()
@ -110,13 +122,15 @@ class GitManager @Inject constructor(
try {
repository.workTree // test if repository is valid
_repositorySelectionStatus.value = RepositorySelectionStatus.Open(repository)
git = Git(repository)
tabState.git = Git(repository)
onRepositoryChanged(repository.directory.parent)
refreshRepositoryInfo()
launch {
watchRepositoryChanges()
}
println("AppStateManagerReference $appStateManager")
} catch (ex: Exception) {
ex.printStackTrace()
onRepositoryChanged(null)
@ -125,6 +139,10 @@ class GitManager @Inject constructor(
}
}
suspend fun loadRepositoryState(git: Git) = withContext(Dispatchers.IO) {
_repositoryState.value = repositoryManager.getRepositoryState(git)
}
private suspend fun watchRepositoryChanges() {
val ignored = safeGit.status().call().ignoredNotInIndex.toList()
@ -135,78 +153,35 @@ class GitManager @Inject constructor(
if (!operationRunning) { // Only update if there isn't any process running
safeProcessing(showError = false) {
println("Changes detected, loading status")
val hasUncommitedChanges = statusManager.hasUncommitedChanges.value
statusManager.loadHasUncommitedChanges(safeGit)
statusManager.loadStatus(safeGit)
// val hasUncommitedChanges = statusManager.hasUncommitedChanges.value
// statusManager.loadHasUncommitedChanges(safeGit)
// statusManager.loadStatus(safeGit)
if(!hasUncommitedChanges) {
logManager.loadLog(safeGit)
}
statusViewModel.refresh(safeGit)
checkUncommitedChanges()
}
}
}
}
fun loadLog() = managerScope.launch {
coLoadLog()
private suspend fun loadLog() {
logViewModel.loadLog(safeGit)
}
private suspend fun coLoadLog() {
logManager.loadLog(safeGit)
}
suspend fun loadStatus() {
val hadUncommitedChanges = statusManager.hasUncommitedChanges.value
statusManager.loadStatus(safeGit)
val hasNowUncommitedChanges = statusManager.hasUncommitedChanges.value
suspend fun checkUncommitedChanges() {
val uncommitedChangesStateChanged = statusViewModel.updateHasUncommitedChanges(safeGit)
// Update the log only if the uncommitedChanges status has changed
if (hasNowUncommitedChanges != hadUncommitedChanges)
coLoadLog()
if (uncommitedChangesStateChanged)
loadLog()
}
fun stage(diffEntry: DiffEntry) = managerScope.launch {
runOperation {
statusManager.stage(safeGit, diffEntry)
}
}
fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = managerScope.launch {
runOperation {
statusManager.stageHunk(safeGit, diffEntry, hunk)
}
}
fun unstageHunk(diffEntry: DiffEntry, hunk: Hunk) = managerScope.launch {
runOperation {
statusManager.unstageHunk(safeGit, diffEntry, hunk)
}
}
fun unstage(diffEntry: DiffEntry) = managerScope.launch {
runOperation {
statusManager.unstage(safeGit, diffEntry)
}
}
fun commit(message: String) = managerScope.launch {
safeProcessing {
statusManager.commit(safeGit, message)
refreshRepositoryInfo()
}
}
val hasUncommitedChanges: StateFlow<Boolean>
get() = statusManager.hasUncommitedChanges
suspend fun diffFormat(diffEntryType: DiffEntryType): List<Hunk> {
try {
return diffManager.diffFormat(safeGit, diffEntryType)
} catch (ex: Exception) {
ex.printStackTrace()
loadStatus()
checkUncommitedChanges()
return listOf()
}
}
@ -214,7 +189,7 @@ class GitManager @Inject constructor(
fun pull() = managerScope.launch {
safeProcessing {
remoteOperationsManager.pull(safeGit)
coLoadLog()
loadLog()
}
}
@ -223,26 +198,27 @@ class GitManager @Inject constructor(
try {
remoteOperationsManager.push(safeGit)
} finally {
coLoadLog()
loadLog()
}
}
}
private suspend fun refreshRepositoryInfo() {
statusManager.loadRepositoryStatus(safeGit)
statusManager.loadHasUncommitedChanges(safeGit)
statusManager.loadStatus(safeGit)
branchesManager.loadBranches(safeGit)
remotesManager.loadRemotes(safeGit, branchesManager.remoteBranches(safeGit))
tagsManager.loadTags(safeGit)
logViewModel.refresh(safeGit)
branchesViewModel.refresh(safeGit)
remotesViewModel.refresh(safeGit)
tagsViewModel.refresh(safeGit)
statusViewModel.refresh(safeGit)
loadRepositoryState(safeGit)
stashManager.loadStashList(safeGit)
coLoadLog()
loadLog()
}
fun stash() = managerScope.launch {
safeProcessing {
stashManager.stash(safeGit)
loadStatus()
checkUncommitedChanges()
loadLog()
}
}
@ -250,42 +226,11 @@ class GitManager @Inject constructor(
fun popStash() = managerScope.launch {
safeProcessing {
stashManager.popStash(safeGit)
loadStatus()
checkUncommitedChanges()
loadLog()
}
}
fun createBranch(branchName: String) = managerScope.launch {
safeProcessing {
branchesManager.createBranch(safeGit, branchName)
coLoadLog()
}
}
fun deleteBranch(branch: Ref) = managerScope.launch {
safeProcessing {
branchesManager.deleteBranch(safeGit, branch)
refreshRepositoryInfo()
}
}
fun deleteTag(tag: Ref) = managerScope.launch {
safeProcessing {
tagsManager.deleteTag(safeGit, tag)
refreshRepositoryInfo()
}
}
fun resetStaged(diffEntry: DiffEntry) = managerScope.launch {
statusManager.reset(safeGit, diffEntry, staged = true)
loadLog()
}
fun resetUnstaged(diffEntry: DiffEntry) = managerScope.launch {
statusManager.reset(safeGit, diffEntry, staged = false)
loadLog()
}
fun credentialsDenied() {
credentialsStateManager.updateState(CredentialsState.CredentialsDenied)
}
@ -302,68 +247,8 @@ class GitManager @Inject constructor(
return diffManager.commitDiffEntries(safeGit, commit)
}
fun unstageAll() = managerScope.launch {
safeProcessing {
statusManager.unstageAll(safeGit)
}
}
fun stageAll() = managerScope.launch {
safeProcessing {
statusManager.stageAll(safeGit)
}
}
fun checkoutCommit(revCommit: RevCommit) = managerScope.launch {
safeProcessing {
logManager.checkoutCommit(safeGit, revCommit)
refreshRepositoryInfo()
}
}
fun revertCommit(revCommit: RevCommit) = managerScope.launch {
safeProcessing {
logManager.revertCommit(safeGit, revCommit)
refreshRepositoryInfo()
}
}
fun resetToCommit(revCommit: RevCommit, resetType: ResetType) = managerScope.launch {
safeProcessing {
logManager.resetToCommit(safeGit, revCommit, resetType = resetType)
refreshRepositoryInfo()
}
}
fun createBranchOnCommit(branch: String, revCommit: RevCommit) = managerScope.launch {
safeProcessing {
branchesManager.createBranchOnCommit(safeGit, branch, revCommit)
refreshRepositoryInfo()
}
}
fun createTagOnCommit(tag: String, revCommit: RevCommit) = managerScope.launch {
safeProcessing {
tagsManager.createTagOnCommit(safeGit, tag, revCommit)
refreshRepositoryInfo()
}
}
var onRepositoryChanged: (path: String?) -> Unit = {}
fun checkoutRef(ref: Ref) = managerScope.launch {
safeProcessing {
logManager.checkoutRef(safeGit, ref)
refreshRepositoryInfo()
}
}
fun mergeBranch(ref: Ref, fastForward: Boolean) = managerScope.launch {
safeProcessing {
branchesManager.mergeBranch(safeGit, ref, fastForward)
refreshRepositoryInfo()
}
}
fun dispose() {
managerScope.cancel()
@ -377,7 +262,6 @@ class GitManager @Inject constructor(
return safeGit.repository.parseCommit(objectId)
}
@Synchronized
private suspend fun safeProcessing(showError: Boolean = true, callback: suspend () -> Unit) {
_processing.value = true
operationRunning = true
@ -395,13 +279,10 @@ class GitManager @Inject constructor(
}
}
private inline fun runOperation(block: () -> Unit) {
operationRunning = true
try {
block()
} finally {
operationRunning = false
}
fun updatedDiffEntry(diffSelected: DiffEntryType) = tabState.runOperation { git ->
diffViewModel.updateDiff(git , diffSelected)
return@runOperation RefreshType.NONE
}
}
@ -410,4 +291,4 @@ sealed class RepositorySelectionStatus {
object None : RepositorySelectionStatus()
object Loading : RepositorySelectionStatus()
data class Open(val repository: Repository) : RepositorySelectionStatus()
}
}

View file

@ -0,0 +1,45 @@
package app.viewmodels
import app.git.BranchesManager
import app.git.RefreshType
import app.git.TabState
import app.git.TagsManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class TagsViewModel @Inject constructor(
private val tabState: TabState,
private val branchesManager: BranchesManager,
private val tagsManager: TagsManager,
) {
private val _tags = MutableStateFlow<List<Ref>>(listOf())
val tags: StateFlow<List<Ref>>
get() = _tags
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)
}
}