Completed arch refactor

This commit is contained in:
Abdelilah El Aissaoui 2022-01-04 19:54:56 +01:00
parent e7de563b28
commit 97a082bc47
19 changed files with 335 additions and 294 deletions

View File

@ -36,7 +36,7 @@ dependencies {
tasks.withType<KotlinCompile>() { tasks.withType<KotlinCompile>() {
kotlinOptions.jvmTarget = "11" kotlinOptions.jvmTarget = "11"
kotlinOptions.allWarningsAsErrors = true kotlinOptions.allWarningsAsErrors = false
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
} }

View File

@ -149,6 +149,14 @@ class App {
} }
private fun removeTab(tabs: List<TabInformation>, key: Int) = appScope.launch(Dispatchers.IO) { 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 } tabsFlow.value = tabs.filter { tab -> tab.key != key }
} }
@ -187,10 +195,7 @@ class App {
onAddedTab(newAppTab) onAddedTab(newAppTab)
newAppTab newAppTab
}, },
onTabClosed = { key -> onTabClosed = onRemoveTab
appStateManager.repositoryTabRemoved(key)
onRemoveTab(key)
}
) )
IconButton( IconButton(
modifier = Modifier modifier = Modifier

View File

@ -5,21 +5,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject import javax.inject.Inject
class StashManager @Inject constructor() { 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) { suspend fun stash(git: Git) = withContext(Dispatchers.IO) {
git git
.stashCreate() .stashCreate()
.setIncludeUntracked(true) .setIncludeUntracked(true)
.call() .call()
loadStashList(git)
} }
suspend fun popStash(git: Git) = withContext(Dispatchers.IO) { suspend fun popStash(git: Git) = withContext(Dispatchers.IO) {
@ -29,23 +22,11 @@ class StashManager @Inject constructor() {
git.stashDrop() git.stashDrop()
.call() .call()
loadStashList(git)
} }
suspend fun loadStashList(git: Git) = withContext(Dispatchers.IO) { suspend fun getStashList(git: Git) = withContext(Dispatchers.IO) {
_stashStatus.value = StashStatus.Loading return@withContext git
val stashList = git
.stashList() .stashList()
.call() .call()
_stashStatus.value = StashStatus.Loaded(stashList.toList())
} }
} }
sealed class StashStatus {
object Loading : StashStatus()
data class Loaded(val stashes: List<RevCommit>) : StashStatus()
}

View File

@ -4,6 +4,7 @@ import app.app.Error
import app.app.newErrorNow import app.app.newErrorNow
import app.di.TabScope import app.di.TabScope
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -46,13 +47,11 @@ class TabState @Inject constructor() {
@set:Synchronized @set:Synchronized
var operationRunning = false var operationRunning = false
private val _processing = MutableStateFlow(false) private val _processing = MutableStateFlow(false)
val processing: StateFlow<Boolean> val processing: StateFlow<Boolean> = _processing
get() = _processing
fun safeProcessing(showError: Boolean = true, callback: suspend (git: Git) -> RefreshType) = fun safeProcessing(showError: Boolean = true, callback: suspend (git: Git) -> RefreshType) =
managerScope.launch { managerScope.launch(Dispatchers.IO) {
mutex.withLock { mutex.withLock {
_processing.value = true _processing.value = true
operationRunning = true operationRunning = true
@ -74,7 +73,30 @@ class TabState @Inject constructor() {
} }
} }
fun runOperation(block: suspend (git: Git) -> RefreshType) = managerScope.launch { 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 operationRunning = true
try { try {
val refreshType = block(safeGit) val refreshType = block(safeGit)

View File

@ -27,33 +27,15 @@ import app.ui.dialogs.PasswordDialog
import app.ui.dialogs.UserPasswordDialog import app.ui.dialogs.UserPasswordDialog
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
// TODO onDispose sometimes is called when changing tabs, therefore losing the tab state
@OptIn(ExperimentalAnimationApi::class) @OptIn(ExperimentalAnimationApi::class)
@Composable @Composable
fun AppTab( fun AppTab(
gitManager: TabViewModel, tabViewModel: TabViewModel,
repositoryPath: String?,
tabName: MutableState<String>
) { ) {
DisposableEffect(gitManager) { val errorManager = tabViewModel.errorsManager
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 lastError by errorManager.lastError.collectAsState() val lastError by errorManager.lastError.collectAsState()
var showError by remember { mutableStateOf(false) }
var showError by remember {
mutableStateOf(false)
}
if (lastError != null) if (lastError != null)
LaunchedEffect(lastError) { LaunchedEffect(lastError) {
@ -62,13 +44,8 @@ fun AppTab(
showError = false showError = false
} }
val repositorySelectionStatus by tabViewModel.repositorySelectionStatus.collectAsState()
val repositorySelectionStatus by gitManager.repositorySelectionStatus.collectAsState() val isProcessing by tabViewModel.processing.collectAsState()
val isProcessing by gitManager.processing.collectAsState()
if (repositorySelectionStatus is RepositorySelectionStatus.Open) {
tabName.value = gitManager.repositoryName
}
Box { Box {
Column( Column(
@ -87,7 +64,7 @@ fun AppTab(
.alpha(linearProgressAlpha) .alpha(linearProgressAlpha)
) )
CredentialsDialog(gitManager) CredentialsDialog(tabViewModel)
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Crossfade(targetState = repositorySelectionStatus) { Crossfade(targetState = repositorySelectionStatus) {
@ -95,13 +72,13 @@ fun AppTab(
@Suppress("UnnecessaryVariable") // Don't inline it because smart cast won't work @Suppress("UnnecessaryVariable") // Don't inline it because smart cast won't work
when (repositorySelectionStatus) { when (repositorySelectionStatus) {
RepositorySelectionStatus.None -> { RepositorySelectionStatus.None -> {
WelcomePage(gitManager = gitManager) WelcomePage(tabViewModel = tabViewModel)
} }
RepositorySelectionStatus.Loading -> { RepositorySelectionStatus.Loading -> {
LoadingRepository() LoadingRepository()
} }
is RepositorySelectionStatus.Open -> { is RepositorySelectionStatus.Open -> {
RepositoryOpenPage(tabViewModel = gitManager) RepositoryOpenPage(tabViewModel = tabViewModel)
} }
} }
} }

View File

@ -4,10 +4,7 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Divider import androidx.compose.material.*
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -25,20 +22,38 @@ import app.theme.secondaryTextColor
import app.ui.components.AvatarImage import app.ui.components.AvatarImage
import app.ui.components.ScrollableLazyColumn import app.ui.components.ScrollableLazyColumn
import app.ui.components.TooltipText import app.ui.components.TooltipText
import app.viewmodels.CommitChangesStatus
import app.viewmodels.CommitChangesViewModel
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
@Composable @Composable
fun CommitChanges( fun CommitChanges(
gitManager: TabViewModel, commitChangesViewModel: CommitChangesViewModel,
commit: RevCommit,
onDiffSelected: (DiffEntry) -> Unit onDiffSelected: (DiffEntry) -> Unit
) { ) {
var diff by remember { mutableStateOf(emptyList<DiffEntry>()) } val commitChangesStatusState = commitChangesViewModel.commitChangesStatus.collectAsState()
LaunchedEffect(commit) {
diff = gitManager.diffListFromCommit(commit)
}
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( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
@ -90,7 +105,7 @@ fun CommitChanges(
) )
CommitLogChanges(diff, onDiffSelected = onDiffSelected) CommitLogChanges(changes, onDiffSelected = onDiffSelected)
} }
} }
} }

View File

@ -17,14 +17,12 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.viewmodels.MenuViewModel
@Composable @Composable
fun GMenu( fun Menu(
menuViewModel: MenuViewModel,
onRepositoryOpen: () -> Unit, onRepositoryOpen: () -> Unit,
onPull: () -> Unit,
onPush: () -> Unit,
onStash: () -> Unit,
onPopStash: () -> Unit,
onCreateBranch: () -> Unit, onCreateBranch: () -> Unit,
) { ) {
Row( Row(
@ -47,17 +45,13 @@ fun GMenu(
MenuButton( MenuButton(
title = "Pull", title = "Pull",
icon = painterResource("download.svg"), icon = painterResource("download.svg"),
onClick = { onClick = { menuViewModel.pull() },
onPull()
},
) )
MenuButton( MenuButton(
title = "Push", title = "Push",
icon = painterResource("upload.svg"), icon = painterResource("upload.svg"),
onClick = { onClick = { menuViewModel.push() },
onPush()
},
) )
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@ -76,12 +70,12 @@ fun GMenu(
MenuButton( MenuButton(
title = "Stash", title = "Stash",
icon = painterResource("stash.svg"), icon = painterResource("stash.svg"),
onClick = onStash, onClick = { menuViewModel.stash() },
) )
MenuButton( MenuButton(
title = "Pop", title = "Pop",
icon = painterResource("apply_stash.svg"), icon = painterResource("apply_stash.svg"),
onClick = onPopStash, onClick = { menuViewModel.popStash() },
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))

View File

@ -20,10 +20,9 @@ import org.jetbrains.compose.splitpane.rememberSplitPaneState
fun RepositoryOpenPage(tabViewModel: TabViewModel) { fun RepositoryOpenPage(tabViewModel: TabViewModel) {
val repositoryState by tabViewModel.repositoryState.collectAsState() val repositoryState by tabViewModel.repositoryState.collectAsState()
val diffSelected by tabViewModel.diffSelected.collectAsState() val diffSelected by tabViewModel.diffSelected.collectAsState()
val selectedItem by tabViewModel.selectedItem.collectAsState()
var showNewBranchDialog by remember { mutableStateOf(false) } var showNewBranchDialog by remember { mutableStateOf(false) }
val (selectedItem, setSelectedItem) = remember { mutableStateOf<SelectedItem>(SelectedItem.None) }
LaunchedEffect(selectedItem) { LaunchedEffect(selectedItem) {
tabViewModel.newDiffSelected = null tabViewModel.newDiffSelected = null
} }
@ -41,14 +40,11 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
} }
Column { Column {
GMenu( Menu(
menuViewModel = tabViewModel.menuViewModel,
onRepositoryOpen = { onRepositoryOpen = {
openRepositoryDialog(gitManager = tabViewModel) openRepositoryDialog(gitManager = tabViewModel)
}, },
onPull = { tabViewModel.pull() },
onPush = { tabViewModel.push() },
onStash = { tabViewModel.stash() },
onPopStash = { tabViewModel.popStash() },
onCreateBranch = { showNewBranchDialog = true } onCreateBranch = { showNewBranchDialog = true }
) )
@ -64,22 +60,20 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
Branches( Branches(
branchesViewModel = tabViewModel.branchesViewModel, branchesViewModel = tabViewModel.branchesViewModel,
onBranchClicked = { onBranchClicked = {
val commit = tabViewModel.findCommit(it.objectId) tabViewModel.newSelectedRef(it.objectId)
setSelectedItem(SelectedItem.Ref(commit))
} }
) )
Remotes(remotesViewModel = tabViewModel.remotesViewModel) Remotes(remotesViewModel = tabViewModel.remotesViewModel)
Tags( Tags(
tagsViewModel = tabViewModel.tagsViewModel, tagsViewModel = tabViewModel.tagsViewModel,
onTagClicked = { onTagClicked = {
val commit = tabViewModel.findCommit(it.objectId) tabViewModel.newSelectedRef(it.objectId)
setSelectedItem(SelectedItem.Ref(commit))
} }
) )
Stashes( Stashes(
gitManager = tabViewModel, stashesViewModel = tabViewModel.stashesViewModel,
onStashSelected = { stash -> onStashSelected = { stash ->
setSelectedItem(SelectedItem.Stash(stash)) tabViewModel.newSelectedStash(stash)
} }
) )
} }
@ -102,7 +96,7 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
logViewModel = tabViewModel.logViewModel, logViewModel = tabViewModel.logViewModel,
selectedItem = selectedItem, selectedItem = selectedItem,
onItemSelected = { onItemSelected = {
setSelectedItem(it) tabViewModel.newSelectedItem(it)
}, },
) )
} }
@ -120,7 +114,8 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
) { ) {
if (selectedItem == SelectedItem.UncommitedChanges) { val safeSelectedItem = selectedItem
if (safeSelectedItem == SelectedItem.UncommitedChanges) {
UncommitedChanges( UncommitedChanges(
statusViewModel = tabViewModel.statusViewModel, statusViewModel = tabViewModel.statusViewModel,
selectedEntryType = diffSelected, selectedEntryType = diffSelected,
@ -135,10 +130,9 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
tabViewModel.newDiffSelected = DiffEntryType.UnstagedDiff(diffEntry) tabViewModel.newDiffSelected = DiffEntryType.UnstagedDiff(diffEntry)
} }
) )
} else if (selectedItem is SelectedItem.CommitBasedItem) { } else if (safeSelectedItem is SelectedItem.CommitBasedItem) {
CommitChanges( CommitChanges(
gitManager = tabViewModel, commitChangesViewModel = tabViewModel.commitChangesViewModel,
commit = selectedItem.revCommit,
onDiffSelected = { diffEntry -> onDiffSelected = { diffEntry ->
tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry) tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry)
} }

View File

@ -6,19 +6,19 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import app.viewmodels.TabViewModel
import app.git.StashStatus
import app.ui.components.ScrollableLazyColumn import app.ui.components.ScrollableLazyColumn
import app.ui.components.SideMenuEntry import app.ui.components.SideMenuEntry
import app.ui.components.SideMenuSubentry import app.ui.components.SideMenuSubentry
import app.viewmodels.StashStatus
import app.viewmodels.StashesViewModel
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
@Composable @Composable
fun Stashes( fun Stashes(
gitManager: TabViewModel, stashesViewModel: StashesViewModel,
onStashSelected: (commit: RevCommit) -> Unit, onStashSelected: (commit: RevCommit) -> Unit,
) { ) {
val stashStatusState = gitManager.stashStatus.collectAsState() val stashStatusState = stashesViewModel.stashStatus.collectAsState()
val stashStatus = stashStatusState.value val stashStatus = stashStatusState.value
val stashList = if (stashStatus is StashStatus.Loaded) val stashList = if (stashStatus is StashStatus.Loaded)

View File

@ -29,7 +29,7 @@ fun openRepositoryDialog(gitManager: TabViewModel) {
} }
private fun openRepositoryDialog( private fun openRepositoryDialog(
gitManager: TabViewModel, tabViewModel: TabViewModel,
latestDirectoryOpened: String latestDirectoryOpened: String
) { ) {
@ -42,5 +42,5 @@ private fun openRepositoryDialog(
fileChooser.showSaveDialog(null) fileChooser.showSaveDialog(null)
if (fileChooser.selectedFile != null) if (fileChooser.selectedFile != null)
gitManager.openRepository(fileChooser.selectedFile) tabViewModel.openRepository(fileChooser.selectedFile)
} }

View File

@ -33,9 +33,9 @@ import java.net.URI
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun WelcomePage( fun WelcomePage(
gitManager: TabViewModel, tabViewModel: TabViewModel,
) { ) {
val appStateManager = gitManager.appStateManager val appStateManager = tabViewModel.appStateManager
var showCloneView by remember { mutableStateOf(false) } var showCloneView by remember { mutableStateOf(false) }
// Crossfade(showCloneView) { // Crossfade(showCloneView) {
@ -69,7 +69,7 @@ fun WelcomePage(
.padding(bottom = 8.dp), .padding(bottom = 8.dp),
title = "Open a repository", title = "Open a repository",
painter = painterResource("open.svg"), painter = painterResource("open.svg"),
onClick = { openRepositoryDialog(gitManager) } onClick = { openRepositoryDialog(tabViewModel) }
) )
ButtonTile( ButtonTile(
@ -136,7 +136,7 @@ fun WelcomePage(
) { ) {
TextButton( TextButton(
onClick = { onClick = {
gitManager.openRepository(repo) tabViewModel.openRepository(repo)
} }
) { ) {
Text( Text(
@ -161,7 +161,7 @@ fun WelcomePage(
if (showCloneView) if (showCloneView)
MaterialDialog { MaterialDialog {
CloneDialog(gitManager, onClose = { showCloneView = false }) CloneDialog(tabViewModel, onClose = { showCloneView = false })
} }
// Popup(focusable = true, onDismissRequest = { showCloneView = false }, alignment = Alignment.Center) { // Popup(focusable = true, onDismissRequest = { showCloneView = false }, alignment = Alignment.Center) {
// //

View File

@ -28,6 +28,8 @@ import app.theme.tabColorActive
import app.theme.tabColorInactive import app.theme.tabColorInactive
import app.ui.AppTab import app.ui.AppTab
import javax.inject.Inject import javax.inject.Inject
import kotlin.io.path.Path
import kotlin.io.path.name
@Composable @Composable
@ -166,7 +168,7 @@ class TabInformation(
appComponent: AppComponent, appComponent: AppComponent,
) { ) {
@Inject @Inject
lateinit var gitManager: TabViewModel lateinit var tabViewModel: TabViewModel
@Inject @Inject
lateinit var appStateManager: AppStateManager lateinit var appStateManager: AppStateManager
@ -180,14 +182,18 @@ class TabInformation(
tabComponent.inject(this) tabComponent.inject(this)
//TODO: This shouldn't be here, should be in the parent method //TODO: This shouldn't be here, should be in the parent method
gitManager.onRepositoryChanged = { path -> tabViewModel.onRepositoryChanged = { path ->
if (path == null) { if (path == null) {
appStateManager.repositoryTabRemoved(key) appStateManager.repositoryTabRemoved(key)
} else } else {
tabName.value = Path(path).name
appStateManager.repositoryTabChanged(key, path) appStateManager.repositoryTabChanged(key, path)
}
} }
if(path != null)
tabViewModel.openRepository(path)
content = { content = {
AppTab(gitManager, path, tabName) AppTab(tabViewModel)
} }
} }
} }

View File

@ -0,0 +1,35 @@
package app.viewmodels
import app.git.DiffManager
import app.git.RefreshType
import app.git.TabState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
class CommitChangesViewModel @Inject constructor(
private val tabState: TabState,
private val diffManager: DiffManager,
) {
private val _commitChangesStatus = MutableStateFlow<CommitChangesStatus>(CommitChangesStatus.Loading)
val commitChangesStatus: StateFlow<CommitChangesStatus> = _commitChangesStatus
fun loadChanges(commit: RevCommit) = tabState.runOperation { git ->
_commitChangesStatus.value = CommitChangesStatus.Loading
val changes = diffManager.commitDiffEntries(git, commit)
_commitChangesStatus.value = CommitChangesStatus.Loaded(commit, changes)
return@runOperation RefreshType.NONE
}
}
sealed class CommitChangesStatus {
object Loading : CommitChangesStatus()
data class Loaded(val commit: RevCommit, val changes: List<DiffEntry>) : CommitChangesStatus()
}

View File

@ -1,17 +1,10 @@
package app.viewmodels package app.viewmodels
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import app.git.* import app.git.*
import app.git.diff.Hunk import app.git.diff.Hunk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow 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 org.eclipse.jgit.diff.DiffEntry
import javax.inject.Inject import javax.inject.Inject
@ -31,7 +24,7 @@ class DiffViewModel @Inject constructor(
) )
) )
suspend fun updateDiff(git: Git, diffEntryType: DiffEntryType) = withContext(Dispatchers.IO) { fun updateDiff(diffEntryType: DiffEntryType) = tabState.runOperation { git ->
val oldDiffEntryType = _diffResult.value?.diffEntryType val oldDiffEntryType = _diffResult.value?.diffEntryType
_diffResult.value = null _diffResult.value = null
@ -51,6 +44,8 @@ class DiffViewModel @Inject constructor(
val hunks = diffManager.diffFormat(git, diffEntryType) val hunks = diffManager.diffFormat(git, diffEntryType)
_diffResult.value = DiffResult(diffEntryType, hunks) _diffResult.value = DiffResult(diffEntryType, hunks)
return@runOperation RefreshType.NONE
} }
fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation { git -> fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation { git ->

View File

@ -0,0 +1,41 @@
package app.viewmodels
import app.git.RefreshType
import app.git.RemoteOperationsManager
import app.git.StashManager
import app.git.TabState
import javax.inject.Inject
class MenuViewModel @Inject constructor(
private val tabState: TabState,
private val remoteOperationsManager: RemoteOperationsManager,
private val stashManager: StashManager,
) {
fun pull() = tabState.safeProcessing { git ->
remoteOperationsManager.pull(git)
return@safeProcessing RefreshType.ONLY_LOG
}
fun push() = tabState.safeProcessing { git ->
try {
remoteOperationsManager.push(git)
} catch (ex: Exception) {
ex.printStackTrace()
}
return@safeProcessing RefreshType.ONLY_LOG
}
fun stash() = tabState.safeProcessing { git ->
stashManager.stash(git)
return@safeProcessing RefreshType.UNCOMMITED_CHANGES
}
fun popStash() = tabState.safeProcessing { git ->
stashManager.popStash(git)
return@safeProcessing RefreshType.UNCOMMITED_CHANGES
}
}

View File

@ -0,0 +1,32 @@
package app.viewmodels
import app.git.StashManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
class StashesViewModel @Inject constructor(
private val stashManager: StashManager,
) {
private val _stashStatus = MutableStateFlow<StashStatus>(StashStatus.Loaded(listOf()))
val stashStatus: StateFlow<StashStatus>
get() = _stashStatus
suspend fun loadStashes(git: Git) {
_stashStatus.value = StashStatus.Loading
val stashList = stashManager.getStashList(git)
_stashStatus.value = StashStatus.Loaded(stashList.toList()) // TODO: Is the list cast necessary?
}
suspend fun refresh(git: Git) {
loadStashes(git)
}
}
sealed class StashStatus {
object Loading : StashStatus()
data class Loaded(val stashes: List<RevCommit>) : StashStatus()
}

View File

@ -26,9 +26,7 @@ class StatusViewModel @Inject constructor(
_commitMessage.value = value _commitMessage.value = value
} }
private val _hasUncommitedChanges = MutableStateFlow<Boolean>(false) private var lastUncommitedChangesState = false
val hasUncommitedChanges: StateFlow<Boolean>
get() = _hasUncommitedChanges
fun stage(diffEntry: DiffEntry) = tabState.runOperation { git -> fun stage(diffEntry: DiffEntry) = tabState.runOperation { git ->
statusManager.stage(git, diffEntry) statusManager.stage(git, diffEntry)
@ -68,7 +66,7 @@ class StatusViewModel @Inject constructor(
return@runOperation RefreshType.UNCOMMITED_CHANGES return@runOperation RefreshType.UNCOMMITED_CHANGES
} }
suspend fun loadStatus(git: Git) { private suspend fun loadStatus(git: Git) {
val previousStatus = _stageStatus.value val previousStatus = _stageStatus.value
try { try {
@ -85,8 +83,8 @@ class StatusViewModel @Inject constructor(
} }
} }
suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) { private suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) {
_hasUncommitedChanges.value = statusManager.hasUncommitedChanges(git) lastUncommitedChangesState = statusManager.hasUncommitedChanges(git)
} }
fun commit(message: String) = tabState.safeProcessing { git -> fun commit(message: String) = tabState.safeProcessing { git ->
@ -95,7 +93,6 @@ class StatusViewModel @Inject constructor(
return@safeProcessing RefreshType.ALL_DATA return@safeProcessing RefreshType.ALL_DATA
} }
suspend fun refresh(git: Git) = withContext(Dispatchers.IO) { suspend fun refresh(git: Git) = withContext(Dispatchers.IO) {
loadStatus(git) loadStatus(git)
loadHasUncommitedChanges(git) loadHasUncommitedChanges(git)
@ -105,11 +102,12 @@ class StatusViewModel @Inject constructor(
* Checks if there are uncommited changes and returns if the state has changed ( * Checks if there are uncommited changes and returns if the state has changed (
*/ */
suspend fun updateHasUncommitedChanges(git: Git): Boolean { suspend fun updateHasUncommitedChanges(git: Git): Boolean {
val hadUncommitedChanges = hasUncommitedChanges.value val hadUncommitedChanges = this.lastUncommitedChangesState
loadStatus(git) loadStatus(git)
loadHasUncommitedChanges(git)
val hasNowUncommitedChanges = hasUncommitedChanges.value val hasNowUncommitedChanges = this.lastUncommitedChangesState
// Return true to update the log only if the uncommitedChanges status has changed // Return true to update the log only if the uncommitedChanges status has changed
return (hasNowUncommitedChanges != hadUncommitedChanges) return (hasNowUncommitedChanges != hadUncommitedChanges)

View File

@ -6,12 +6,12 @@ import app.app.newErrorNow
import app.credentials.CredentialsState import app.credentials.CredentialsState
import app.credentials.CredentialsStateManager import app.credentials.CredentialsStateManager
import app.git.* import app.git.*
import app.ui.SelectedItem
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.Repository import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.lib.RepositoryState
@ -28,40 +28,35 @@ class TabViewModel @Inject constructor(
val remotesViewModel: RemotesViewModel, val remotesViewModel: RemotesViewModel,
val statusViewModel: StatusViewModel, val statusViewModel: StatusViewModel,
val diffViewModel: DiffViewModel, val diffViewModel: DiffViewModel,
val menuViewModel: MenuViewModel,
val stashesViewModel: StashesViewModel,
val commitChangesViewModel: CommitChangesViewModel,
private val repositoryManager: RepositoryManager, private val repositoryManager: RepositoryManager,
private val remoteOperationsManager: RemoteOperationsManager, private val remoteOperationsManager: RemoteOperationsManager,
private val stashManager: StashManager,
private val diffManager: DiffManager,
private val tabState: TabState, private val tabState: TabState,
val errorsManager: ErrorsManager, val errorsManager: ErrorsManager,
val appStateManager: AppStateManager, val appStateManager: AppStateManager,
private val fileChangesWatcher: FileChangesWatcher, private val fileChangesWatcher: FileChangesWatcher,
) { ) {
private val _selectedItem = MutableStateFlow<SelectedItem>(SelectedItem.None)
val repositoryName: String val selectedItem: StateFlow<SelectedItem> = _selectedItem
get() = safeGit.repository.directory.parentFile.name
private val credentialsStateManager = CredentialsStateManager private val credentialsStateManager = CredentialsStateManager
private val managerScope = CoroutineScope(SupervisorJob())
private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None) private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None)
val repositorySelectionStatus: StateFlow<RepositorySelectionStatus> val repositorySelectionStatus: StateFlow<RepositorySelectionStatus>
get() = _repositorySelectionStatus get() = _repositorySelectionStatus
private val _processing = MutableStateFlow(false) val processing: StateFlow<Boolean> = tabState.processing
val processing: StateFlow<Boolean>
get() = _processing
val stashStatus: StateFlow<StashStatus> = stashManager.stashStatus
val credentialsState: StateFlow<CredentialsState> = credentialsStateManager.credentialsState val credentialsState: StateFlow<CredentialsState> = credentialsStateManager.credentialsState
val cloneStatus: StateFlow<CloneStatus> = remoteOperationsManager.cloneStatus val cloneStatus: StateFlow<CloneStatus> = remoteOperationsManager.cloneStatus
private val _diffSelected = MutableStateFlow<DiffEntryType?>(null) private val _diffSelected = MutableStateFlow<DiffEntryType?>(null)
val diffSelected : StateFlow<DiffEntryType?> = _diffSelected val diffSelected: StateFlow<DiffEntryType?> = _diffSelected
var newDiffSelected: DiffEntryType? var newDiffSelected: DiffEntryType?
get() = diffSelected.value get() = diffSelected.value
set(value){ set(value) {
_diffSelected.value = value _diffSelected.value = value
updateDiffEntry() updateDiffEntry()
@ -71,13 +66,13 @@ class TabViewModel @Inject constructor(
val repositoryState: StateFlow<RepositoryState> = _repositoryState val repositoryState: StateFlow<RepositoryState> = _repositoryState
init { init {
managerScope.launch { tabState.managerScope.launch {
tabState.refreshData.collect { refreshType -> tabState.refreshData.collect { refreshType ->
when (refreshType) { when (refreshType) {
RefreshType.NONE -> println("Not refreshing...") RefreshType.NONE -> println("Not refreshing...")
RefreshType.ALL_DATA -> refreshRepositoryInfo() RefreshType.ALL_DATA -> refreshRepositoryInfo()
RefreshType.ONLY_LOG -> refreshLog() RefreshType.ONLY_LOG -> refreshLog()
RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges() RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges(false)
} }
} }
} }
@ -89,146 +84,94 @@ class TabViewModel @Inject constructor(
return@runOperation RefreshType.NONE return@runOperation RefreshType.NONE
} }
/**
* Property that indicates if a git operation is running
*/
@set:Synchronized private var operationRunning = false
private val safeGit: Git
get() {
val git = this.tabState.git
if (git == null) {
_repositorySelectionStatus.value = RepositorySelectionStatus.None
throw CancellationException()
} else
return git
}
fun openRepository(directory: String) { fun openRepository(directory: String) {
openRepository(File(directory)) openRepository(File(directory))
} }
fun openRepository(directory: File) = managerScope.launch(Dispatchers.IO) { fun openRepository(directory: File) = tabState.safeProcessingWihoutGit {
safeProcessing { println("Trying to open repository ${directory.absoluteFile}")
println("Trying to open repository ${directory.absoluteFile}")
val gitDirectory = if (directory.name == ".git") { val gitDirectory = if (directory.name == ".git") {
directory
} else {
val gitDir = File(directory, ".git")
if (gitDir.exists() && gitDir.isDirectory) {
gitDir
} else
directory 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)
tabState.git = Git(repository)
onRepositoryChanged(repository.directory.parent)
refreshRepositoryInfo()
launch {
watchRepositoryChanges()
}
println("AppStateManagerReference $appStateManager")
} catch (ex: Exception) {
ex.printStackTrace()
onRepositoryChanged(null)
errorsManager.addError(newErrorNow(ex, ex.localizedMessage))
}
} }
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
} }
suspend fun loadRepositoryState(git: Git) = withContext(Dispatchers.IO) { private suspend fun loadRepositoryState(git: Git) = withContext(Dispatchers.IO) {
_repositoryState.value = repositoryManager.getRepositoryState(git) _repositoryState.value = repositoryManager.getRepositoryState(git)
} }
private suspend fun watchRepositoryChanges() { private suspend fun watchRepositoryChanges(git: Git) = tabState.managerScope.launch(Dispatchers.IO) {
val ignored = safeGit.status().call().ignoredNotInIndex.toList() val ignored = git.status().call().ignoredNotInIndex.toList()
fileChangesWatcher.watchDirectoryPath( fileChangesWatcher.watchDirectoryPath(
pathStr = safeGit.repository.directory.parent, pathStr = git.repository.directory.parent,
ignoredDirsPath = ignored, ignoredDirsPath = ignored,
).collect { ).collect {
if (!operationRunning) { // Only update if there isn't any process running if (!tabState.operationRunning) { // Only update if there isn't any process running
safeProcessing(showError = false) { println("Changes detected, loading status")
println("Changes detected, loading status") checkUncommitedChanges(isFsChange = true)
statusViewModel.refresh(safeGit)
checkUncommitedChanges()
updateDiffEntry() updateDiffEntry()
}
} }
} }
} }
private suspend fun loadLog() { private suspend fun checkUncommitedChanges(isFsChange: Boolean = false) = tabState.runOperation { git ->
logViewModel.loadLog(safeGit) val uncommitedChangesStateChanged = statusViewModel.updateHasUncommitedChanges(git)
}
suspend fun checkUncommitedChanges() {
val uncommitedChangesStateChanged = statusViewModel.updateHasUncommitedChanges(safeGit)
// Update the log only if the uncommitedChanges status has changed // Update the log only if the uncommitedChanges status has changed
if (uncommitedChangesStateChanged) if ((uncommitedChangesStateChanged && isFsChange) || !isFsChange)
loadLog() logViewModel.refresh(git)
updateDiffEntry() 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
} }
fun pull() = managerScope.launch { private suspend fun refreshRepositoryInfo() = tabState.safeProcessing { git ->
safeProcessing { logViewModel.refresh(git)
remoteOperationsManager.pull(safeGit) branchesViewModel.refresh(git)
loadLog() remotesViewModel.refresh(git)
} tagsViewModel.refresh(git)
} statusViewModel.refresh(git)
stashesViewModel.refresh(git)
loadRepositoryState(git)
fun push() = managerScope.launch { return@safeProcessing RefreshType.NONE
safeProcessing {
try {
remoteOperationsManager.push(safeGit)
} finally {
loadLog()
}
}
}
private suspend fun refreshRepositoryInfo() {
logViewModel.refresh(safeGit)
branchesViewModel.refresh(safeGit)
remotesViewModel.refresh(safeGit)
tagsViewModel.refresh(safeGit)
statusViewModel.refresh(safeGit)
loadRepositoryState(safeGit)
stashManager.loadStashList(safeGit)
loadLog()
}
fun stash() = managerScope.launch {
safeProcessing {
stashManager.stash(safeGit)
checkUncommitedChanges()
loadLog()
}
}
fun popStash() = managerScope.launch {
safeProcessing {
stashManager.popStash(safeGit)
checkUncommitedChanges()
loadLog()
}
} }
fun credentialsDenied() { fun credentialsDenied() {
@ -243,51 +186,54 @@ class TabViewModel @Inject constructor(
credentialsStateManager.updateState(CredentialsState.SshCredentialsAccepted(password)) credentialsStateManager.updateState(CredentialsState.SshCredentialsAccepted(password))
} }
suspend fun diffListFromCommit(commit: RevCommit): List<DiffEntry> {
return diffManager.commitDiffEntries(safeGit, commit)
}
var onRepositoryChanged: (path: String?) -> Unit = {} var onRepositoryChanged: (path: String?) -> Unit = {}
fun dispose() { fun dispose() {
managerScope.cancel() tabState.managerScope.cancel()
} }
fun clone(directory: File, url: String) = managerScope.launch { fun clone(directory: File, url: String) = tabState.safeProcessingWihoutGit {
remoteOperationsManager.clone(directory, url) remoteOperationsManager.clone(directory, url)
return@safeProcessingWihoutGit RefreshType.NONE
} }
fun findCommit(objectId: ObjectId): RevCommit { private fun findCommit(git: Git, objectId: ObjectId): RevCommit {
return safeGit.repository.parseCommit(objectId) return git.repository.parseCommit(objectId)
} }
private suspend fun safeProcessing(showError: Boolean = true, callback: suspend () -> Unit) { private fun updateDiffEntry() {
_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
}
}
fun updateDiffEntry() = tabState.runOperation { git ->
val diffSelected = diffSelected.value val diffSelected = diffSelected.value
if(diffSelected != null) { if (diffSelected != null) {
diffViewModel.updateDiff(git, diffSelected) 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 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)
}
}
} }

View File

@ -21,7 +21,7 @@ class TagsViewModel @Inject constructor(
val tags: StateFlow<List<Ref>> val tags: StateFlow<List<Ref>>
get() = _tags get() = _tags
suspend fun loadTags(git: Git) = withContext(Dispatchers.IO) { private suspend fun loadTags(git: Git) = withContext(Dispatchers.IO) {
val tagsList = tagsManager.getTags(git) val tagsList = tagsManager.getTags(git)
_tags.value = tagsList _tags.value = tagsList