Refactored user feedback.

Added warning/error messages
Change UI design
This commit is contained in:
Abdelilah El Aissaoui 2024-08-02 16:24:55 +02:00
parent 20830be926
commit e59fe7df69
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
25 changed files with 268 additions and 83 deletions

View File

@ -0,0 +1,3 @@
package com.jetpackduba.gitnuro.exceptions
class ConflictsException(message: String) : GitnuroException(message)

View File

@ -7,7 +7,7 @@ import kotlinx.coroutines.*
* Use case: Sometimes is not worth updating the UI with a state to "loading" if the load code executed afterwards is really
* fast.
*/
suspend fun delayedStateChange(delayMs: Long, onDelayTriggered: suspend () -> Unit, block: suspend () -> Unit) {
suspend fun <T> delayedStateChange(delayMs: Long, onDelayTriggered: suspend () -> Unit, block: suspend () -> T): T {
val scope = CoroutineScope(Dispatchers.IO)
var completed = false
@ -17,9 +17,10 @@ suspend fun delayedStateChange(delayMs: Long, onDelayTriggered: suspend () -> Un
onDelayTriggered()
}
}
try {
block()
return try {
val result = block()
scope.cancel()
result
} finally {
completed = true
}

View File

@ -8,6 +8,7 @@ import com.jetpackduba.gitnuro.git.log.FindCommitUseCase
import com.jetpackduba.gitnuro.logging.printError
import com.jetpackduba.gitnuro.managers.ErrorsManager
import com.jetpackduba.gitnuro.managers.newErrorNow
import com.jetpackduba.gitnuro.models.Notification
import com.jetpackduba.gitnuro.ui.SelectedItem
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@ -77,12 +78,11 @@ class TabState @Inject constructor(
title: String = "",
subtitle: String = "",
taskType: TaskType,
positiveFeedbackText: String?,
// TODO For now have it always as false because the data refresh is cancelled even when the git process couldn't be cancelled
isCancellable: Boolean = false,
refreshEvenIfCrashes: Boolean = false,
refreshEvenIfCrashesInteractive: ((Exception) -> Boolean)? = null,
callback: suspend (git: Git) -> Unit
callback: suspend (git: Git) -> Notification?,
): Job {
val job = scope.launch(Dispatchers.IO) {
var hasProcessFailed = false
@ -91,7 +91,7 @@ class TabState @Inject constructor(
try {
delayedStateChange(
val notification = delayedStateChange(
delayMs = 300,
onDelayTriggered = {
_processing.update { processingState ->
@ -106,8 +106,8 @@ class TabState @Inject constructor(
callback(git)
}
if (positiveFeedbackText != null) {
errorsManager.emitPositiveNotification(positiveFeedbackText)
if (notification != null) {
errorsManager.emitNotification(notification)
}
} catch (ex: Exception) {
hasProcessFailed = true

View File

@ -1,5 +1,6 @@
package com.jetpackduba.gitnuro.git.branches
import com.jetpackduba.gitnuro.exceptions.ConflictsException
import com.jetpackduba.gitnuro.exceptions.UncommittedChangesDetectedException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -10,6 +11,9 @@ import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class MergeBranchUseCase @Inject constructor() {
/**
* @return true if success has conflicts, false if success without conflicts
*/
suspend operator fun invoke(git: Git, branch: Ref, fastForward: Boolean) = withContext(Dispatchers.IO) {
val fastForwardMode = if (fastForward)
MergeCommand.FastForwardMode.FF
@ -25,5 +29,7 @@ class MergeBranchUseCase @Inject constructor() {
if (mergeResult.mergeStatus == MergeResult.MergeStatus.FAILED) {
throw UncommittedChangesDetectedException("Merge failed, makes sure you repository doesn't contain uncommitted changes.")
}
mergeResult.mergeStatus == MergeResult.MergeStatus.CONFLICTING
}
}

View File

@ -1,9 +1,11 @@
package com.jetpackduba.gitnuro.git.rebase
import com.jetpackduba.gitnuro.exceptions.ConflictsException
import com.jetpackduba.gitnuro.exceptions.UncommittedChangesDetectedException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.MergeResult
import org.eclipse.jgit.api.RebaseCommand
import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.lib.Ref
@ -19,5 +21,11 @@ class RebaseBranchUseCase @Inject constructor() {
if (rebaseResult.status == RebaseResult.Status.UNCOMMITTED_CHANGES) {
throw UncommittedChangesDetectedException("Rebase failed, the repository contains uncommitted changes.")
}
when (rebaseResult.status) {
RebaseResult.Status.UNCOMMITTED_CHANGES -> throw UncommittedChangesDetectedException("Merge failed, makes sure you repository doesn't contain uncommitted changes.")
RebaseResult.Status.CONFLICTS -> throw ConflictsException("Rebase produced conflicts, please fix them to continue.")
else -> {}
}
}
}

View File

@ -1,12 +1,13 @@
package com.jetpackduba.gitnuro.git.stash
import com.jetpackduba.gitnuro.models.Success
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import javax.inject.Inject
class StashChangesUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, message: String?): Unit = withContext(Dispatchers.IO) {
suspend operator fun invoke(git: Git, message: String?): Success = withContext(Dispatchers.IO) {
val commit = git
.stashCreate()
.setIncludeUntracked(true)
@ -16,8 +17,6 @@ class StashChangesUseCase @Inject constructor() {
}
.call()
if (commit == null) {
throw Exception("No changes to stash")
}
commit != null
}
}

View File

@ -4,11 +4,15 @@ import com.jetpackduba.gitnuro.TaskType
import com.jetpackduba.gitnuro.di.TabScope
import com.jetpackduba.gitnuro.exceptions.GitnuroException
import com.jetpackduba.gitnuro.extensions.lockUse
import com.jetpackduba.gitnuro.models.Notification
import com.jetpackduba.gitnuro.models.NotificationType
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
const val NOTIFICATION_DURATION = 2_500L
@TabScope
class ErrorsManager @Inject constructor(
private val coroutineScope: CoroutineScope
@ -20,23 +24,23 @@ class ErrorsManager @Inject constructor(
private val _error = MutableSharedFlow<Error?>()
val error: SharedFlow<Error?> = _error
private val _notification = MutableStateFlow<Map<Long, String>>(hashMapOf())
val notification: StateFlow<Map<Long, String>> = _notification
private val _notification = MutableStateFlow<Map<Long, Notification>>(hashMapOf())
val notification: StateFlow<Map<Long, Notification>> = _notification
private val notificationsMutex = Mutex()
suspend fun emitPositiveNotification(text: String) = coroutineScope.launch {
suspend fun emitNotification(notification: Notification) = coroutineScope.launch {
val time = System.currentTimeMillis()
notificationsMutex.lockUse {
_notification.update { notifications ->
notifications
.toMutableMap()
.apply { put(time, text) }
.apply { put(time, notification) }
}
}
launch {
delay(2000)
delay(NOTIFICATION_DURATION)
notificationsMutex.lockUse {
_notification.update { notifications ->

View File

@ -0,0 +1,14 @@
package com.jetpackduba.gitnuro.models
data class Notification(val type: NotificationType, val text: String)
enum class NotificationType {
Warning,
Positive,
Error,
}
fun positiveNotification(text: String) = Notification(NotificationType.Positive, text)
fun errorNotification(text: String) = Notification(NotificationType.Error, text)
fun warningNotification(text: String) = Notification(NotificationType.Warning, text)

View File

@ -0,0 +1,3 @@
package com.jetpackduba.gitnuro.models
typealias Success = Boolean

View File

@ -7,6 +7,7 @@ val lightTheme = ColorsScheme(
primaryVariant = Color(0xFF0070D8),
onPrimary = Color(0xFFFFFFFFF),
secondary = Color(0xFF9c27b0),
onSecondary = Color(0xFFFFFFFF),
onBackground = Color(0xFF212934),
onBackgroundSecondary = Color(0xFF595858),
error = Color(0xFFc93838),
@ -37,6 +38,7 @@ val darkBlueTheme = ColorsScheme(
primaryVariant = Color(0xFF9FD1FF),
onPrimary = Color(0xFFFFFFFFF),
secondary = Color(0xFFe9c754),
onSecondary = Color(0xFF0E1621),
onBackground = Color(0xFFFFFFFF),
onBackgroundSecondary = Color(0xFFCCCBCB),
error = Color(0xFFc93838),
@ -66,6 +68,7 @@ val darkGrayTheme = ColorsScheme(
primaryVariant = Color(0xFFCDEAFF),
onPrimary = Color(0xFFFFFFFFF),
secondary = Color(0xFFe9c754),
onSecondary = Color(0xFF0E1621),
onBackground = Color(0xFFFFFFFF),
onBackgroundSecondary = Color(0xFFCCCBCB),
error = Color(0xFFc93838),

View File

@ -20,6 +20,7 @@ data class ColorsScheme(
val primaryVariant: Color,
val onPrimary: Color,
val secondary: Color,
val onSecondary: Color,
val onBackground: Color,
val onBackgroundSecondary: Color,
val error: Color,
@ -48,12 +49,12 @@ data class ColorsScheme(
primary = this.primary,
primaryVariant = this.primaryVariant,
secondary = this.secondary,
onSecondary = this.onSecondary,
secondaryVariant = this.secondary,
background = this.background,
surface = this.surface,
error = this.error,
onPrimary = this.onPrimary,
onSecondary = this.onPrimary,
onBackground = this.onBackground,
onSurface = this.onBackground,
onError = this.onError,

View File

@ -1,22 +1,30 @@
package com.jetpackduba.gitnuro.ui
import androidx.compose.animation.Crossfade
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
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.res.painterResource
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.LoadingRepository
import com.jetpackduba.gitnuro.ProcessingScreen
import com.jetpackduba.gitnuro.credentials.CredentialsAccepted
import com.jetpackduba.gitnuro.credentials.CredentialsRequested
import com.jetpackduba.gitnuro.credentials.CredentialsState
import com.jetpackduba.gitnuro.git.ProcessingState
import com.jetpackduba.gitnuro.models.Notification
import com.jetpackduba.gitnuro.models.NotificationType
import com.jetpackduba.gitnuro.theme.AppTheme
import com.jetpackduba.gitnuro.ui.dialogs.CloneDialog
import com.jetpackduba.gitnuro.ui.dialogs.GpgPasswordDialog
import com.jetpackduba.gitnuro.ui.dialogs.SshPasswordDialog
@ -134,17 +142,82 @@ fun AppTab(
.padding(bottom = 48.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
for (notification in notifications)
Text(
text = notification,
modifier = Modifier
.padding(bottom = 12.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colors.primary)
.padding(8.dp),
color = MaterialTheme.colors.onPrimary,
style = MaterialTheme.typography.body1,
)
for (notification in notifications) {
Notification(notification)
}
}
}
}
@Preview
@Composable
fun NotificationSuccessPreview() {
AppTheme(customTheme = null) {
Notification(NotificationType.Positive, "Hello world!")
}
}
@Composable
fun Notification(notification: Notification) {
val notificationShape = RoundedCornerShape(8.dp)
Row(
modifier = Modifier
.padding(8.dp)
.border(2.dp, MaterialTheme.colors.onBackground.copy(0.2f), notificationShape)
.clip(notificationShape)
.background(MaterialTheme.colors.background)
.height(IntrinsicSize.Max)
.padding(2.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
val backgroundColor = when (notification.type) {
NotificationType.Positive -> MaterialTheme.colors.primary
NotificationType.Warning -> MaterialTheme.colors.secondary
NotificationType.Error -> MaterialTheme.colors.error
}
val contentColor = when (notification.type) {
NotificationType.Positive -> MaterialTheme.colors.onPrimary
NotificationType.Warning -> MaterialTheme.colors.onSecondary
NotificationType.Error -> MaterialTheme.colors.onError
}
val icon = when (notification.type) {
NotificationType.Positive -> AppIcons.INFO
NotificationType.Warning -> AppIcons.WARNING
NotificationType.Error -> AppIcons.ERROR
}
Box(
modifier = Modifier
.clip(RoundedCornerShape(topStart = 6.dp, bottomStart = 6.dp))
.background(backgroundColor)
.fillMaxHeight()
// .padding(start = 2.dp, top = 2.dp, bottom = 2.dp)
) {
Icon(
painterResource(icon),
contentDescription = null,
tint = contentColor,
modifier = Modifier
.padding(4.dp)
)
}
Box(
modifier = Modifier
.padding(end = 8.dp)
.fillMaxHeight(),
contentAlignment = Alignment.CenterStart,
) {
Text(
text = notification.text,
modifier = Modifier,
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.body1,
)
}
}
}

View File

@ -80,7 +80,6 @@ class HistoryViewModel @Inject constructor(
title = "History",
subtitle = "Loading file history",
taskType = TaskType.HISTORY_FILE,
positiveFeedbackText = null,
) { git ->
this@HistoryViewModel.filePath = filePath
_historyState.value = HistoryState.Loading(filePath)
@ -91,6 +90,8 @@ class HistoryViewModel @Inject constructor(
.toList()
_historyState.value = HistoryState.Loaded(filePath, log)
null
}
fun selectCommit(commit: RevCommit) = tabState.runOperation(

View File

@ -18,6 +18,7 @@ import com.jetpackduba.gitnuro.git.tags.CreateTagOnCommitUseCase
import com.jetpackduba.gitnuro.git.workspace.CheckHasUncommittedChangesUseCase
import com.jetpackduba.gitnuro.git.workspace.GetStatusSummaryUseCase
import com.jetpackduba.gitnuro.git.workspace.StatusSummary
import com.jetpackduba.gitnuro.models.positiveNotification
import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
import com.jetpackduba.gitnuro.ui.SelectedItem
import com.jetpackduba.gitnuro.ui.log.LogDialog
@ -163,9 +164,10 @@ class LogViewModel @Inject constructor(
title = "Commit checkout",
subtitle = "Checking out commit ${revCommit.name}",
taskType = TaskType.CHECKOUT_COMMIT,
positiveFeedbackText = "Commit checked out",
) { git ->
checkoutCommitUseCase(git, revCommit)
positiveNotification("Commit checked out")
}
fun revertCommit(revCommit: RevCommit) = tabState.safeProcessing(
@ -174,9 +176,10 @@ class LogViewModel @Inject constructor(
subtitle = "Reverting commit ${revCommit.name}",
refreshEvenIfCrashes = true,
taskType = TaskType.REVERT_COMMIT,
positiveFeedbackText = "Commit reverted",
) { git ->
revertCommitUseCase(git, revCommit)
positiveNotification("Commit reverted")
}
fun resetToCommit(revCommit: RevCommit, resetType: ResetType) = tabState.safeProcessing(
@ -184,9 +187,10 @@ class LogViewModel @Inject constructor(
title = "Branch reset",
subtitle = "Resetting branch to commit ${revCommit.shortName}",
taskType = TaskType.RESET_TO_COMMIT,
positiveFeedbackText = "Reset completed",
) { git ->
resetToCommitUseCase(git, revCommit, resetType = resetType)
positiveNotification("Reset completed")
}
fun cherryPickCommit(revCommit: RevCommit) = tabState.safeProcessing(
@ -195,9 +199,10 @@ class LogViewModel @Inject constructor(
subtitle = "Cherry-picking commit ${revCommit.shortName}",
taskType = TaskType.CHERRY_PICK_COMMIT,
refreshEvenIfCrashes = true,
positiveFeedbackText = "Commit cherry-picked"
) { git ->
cherryPickCommitUseCase(git, revCommit)
positiveNotification("Commit cherry-picked")
}
fun createBranchOnCommit(branch: String, revCommit: RevCommit) = tabState.safeProcessing(
@ -206,9 +211,10 @@ class LogViewModel @Inject constructor(
subtitle = "Creating new branch \"$branch\" on commit ${revCommit.shortName}",
refreshEvenIfCrashesInteractive = { it is CheckoutConflictException },
taskType = TaskType.CREATE_BRANCH,
positiveFeedbackText = "Branch \"$branch\" created",
) { git ->
createBranchOnCommitUseCase(git, branch, revCommit)
positiveNotification("Branch \"$branch\" created")
}
fun createTagOnCommit(tag: String, revCommit: RevCommit) = tabState.safeProcessing(
@ -216,9 +222,10 @@ class LogViewModel @Inject constructor(
title = "New tag",
subtitle = "Creating new tag \"$tag\" on commit ${revCommit.shortName}",
taskType = TaskType.CREATE_TAG,
positiveFeedbackText = "Tag created",
) { git ->
createTagOnCommitUseCase(git, tag, revCommit)
positiveNotification("Tag created")
}
private suspend fun uncommittedChangesLoadLog(git: Git) {
@ -375,9 +382,10 @@ class LogViewModel @Inject constructor(
fun rebaseInteractive(revCommit: RevCommit) = tabState.safeProcessing(
refreshType = RefreshType.REBASE_INTERACTIVE_STATE,
taskType = TaskType.REBASE_INTERACTIVE,
positiveFeedbackText = null,
) { git ->
startRebaseInteractiveUseCase(git, revCommit)
null
}
}

View File

@ -11,6 +11,9 @@ import com.jetpackduba.gitnuro.git.stash.PopLastStashUseCase
import com.jetpackduba.gitnuro.git.stash.StashChangesUseCase
import com.jetpackduba.gitnuro.git.workspace.StageUntrackedFileUseCase
import com.jetpackduba.gitnuro.managers.AppStateManager
import com.jetpackduba.gitnuro.models.errorNotification
import com.jetpackduba.gitnuro.models.positiveNotification
import com.jetpackduba.gitnuro.models.warningNotification
import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
import com.jetpackduba.gitnuro.terminal.OpenRepositoryInTerminalUseCase
import javax.inject.Inject
@ -22,10 +25,9 @@ class MenuViewModel @Inject constructor(
private val fetchAllRemotesUseCase: FetchAllRemotesUseCase,
private val popLastStashUseCase: PopLastStashUseCase,
private val stashChangesUseCase: StashChangesUseCase,
private val stageUntrackedFileUseCase: StageUntrackedFileUseCase,
private val openRepositoryInTerminalUseCase: OpenRepositoryInTerminalUseCase,
private val settings: AppSettingsRepository,
private val appStateManager: AppStateManager,
settings: AppSettingsRepository,
appStateManager: AppStateManager,
) {
val isPullWithRebaseDefault = settings.pullRebaseFlow
val lastLoadedTabs = appStateManager.latestOpenedRepositoriesPaths
@ -34,11 +36,12 @@ class MenuViewModel @Inject constructor(
refreshType = RefreshType.ALL_DATA,
title = "Pulling",
subtitle = "Pulling changes from the remote branch to the current branch",
positiveFeedbackText = "Pull completed",
refreshEvenIfCrashes = true,
taskType = TaskType.PULL,
) { git ->
pullBranchUseCase(git, pullType)
positiveNotification("Pull completed")
}
fun fetchAll() = tabState.safeProcessing(
@ -48,9 +51,10 @@ class MenuViewModel @Inject constructor(
isCancellable = false,
refreshEvenIfCrashes = true,
taskType = TaskType.FETCH,
positiveFeedbackText = "Fetch all completed",
) { git ->
fetchAllRemotesUseCase(git)
positiveNotification("Fetch all completed")
}
fun push(force: Boolean = false, pushTags: Boolean = false) = tabState.safeProcessing(
@ -60,26 +64,31 @@ class MenuViewModel @Inject constructor(
isCancellable = false,
refreshEvenIfCrashes = true,
taskType = TaskType.PUSH,
positiveFeedbackText = "Push completed",
) { git ->
pushBranchUseCase(git, force, pushTags)
positiveNotification("Push completed")
}
fun stash() = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITTED_CHANGES_AND_LOG,
taskType = TaskType.STASH,
positiveFeedbackText = "Changes stashed",
) { git ->
stashChangesUseCase(git, null)
if (stashChangesUseCase(git, null)) {
positiveNotification("Changes stashed")
} else {
errorNotification("There are no changes to stash")
}
}
fun popStash() = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITTED_CHANGES_AND_LOG,
refreshEvenIfCrashes = true,
taskType = TaskType.POP_STASH,
positiveFeedbackText = "Stash popped",
) { git ->
popLastStashUseCase(git)
positiveNotification("Stash popped")
}
fun openTerminal() = tabState.runOperation(

View File

@ -67,13 +67,12 @@ class RebaseInteractiveViewModel @Inject constructor(
fun loadRebaseInteractiveData() = tabState.safeProcessing(
refreshType = RefreshType.NONE,
taskType = TaskType.REBASE_INTERACTIVE,// TODO Perhaps this should be more specific such as TaskType.LOAD_ABORT_REBASE
positiveFeedbackText = null,
) { git ->
val state = getRepositoryStateUseCase(git)
if (!state.isRebasing) {
_rebaseState.value = RebaseInteractiveViewState.Loading
return@safeProcessing
return@safeProcessing null
}
try {
@ -107,6 +106,8 @@ class RebaseInteractiveViewModel @Inject constructor(
throw ex
}
}
null
}
private fun isSameRebase(rebaseLines: List<RebaseLine>, state: RebaseInteractiveViewState): Boolean {
@ -126,10 +127,11 @@ class RebaseInteractiveViewModel @Inject constructor(
fun continueRebaseInteractive() = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
taskType = TaskType.REBASE_INTERACTIVE, // TODO Perhaps be more precise with the task type
positiveFeedbackText = null,
) { git ->
resumeRebaseInteractiveUseCase(git, interactiveHandlerContinue)
_rebaseState.value = RebaseInteractiveViewState.Loading
null
}
fun onCommitMessageChanged(commit: AbbreviatedObjectId, message: String) {
@ -181,10 +183,11 @@ class RebaseInteractiveViewModel @Inject constructor(
fun selectLine(line: RebaseLine) = tabState.safeProcessing(
refreshType = RefreshType.NONE,
taskType = TaskType.ABORT_REBASE, // TODO Perhaps be more precise with the task type
positiveFeedbackText = null,
) { git ->
val fullCommit = getCommitFromRebaseLineUseCase(git, line.commit, line.shortMessage)
tabState.newSelectedCommit(fullCommit)
null
}
fun moveCommit(from: Int, to: Int) {

View File

@ -8,6 +8,8 @@ import com.jetpackduba.gitnuro.git.branches.CheckoutRefUseCase
import com.jetpackduba.gitnuro.git.branches.DeleteBranchUseCase
import com.jetpackduba.gitnuro.git.branches.MergeBranchUseCase
import com.jetpackduba.gitnuro.git.rebase.RebaseBranchUseCase
import com.jetpackduba.gitnuro.models.positiveNotification
import com.jetpackduba.gitnuro.models.warningNotification
import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
import kotlinx.coroutines.Job
import org.eclipse.jgit.lib.Ref
@ -34,9 +36,13 @@ class SharedBranchesViewModel @Inject constructor(
title = "Branch merge",
subtitle = "Merging branch ${ref.simpleName}",
taskType = TaskType.MERGE_BRANCH,
positiveFeedbackText = "Merged from \"${ref.simpleName}\"",
refreshEvenIfCrashes = true,
) { git ->
mergeBranchUseCase(git, ref, appSettingsRepository.ffMerge)
if (mergeBranchUseCase(git, ref, appSettingsRepository.ffMerge)) {
warningNotification("Merge produced conflicts, please fix them to continue.")
} else {
positiveNotification("Merged from \"${ref.simpleName}\"")
}
}
override fun deleteBranch(branch: Ref) = tabState.safeProcessing(
@ -44,9 +50,10 @@ class SharedBranchesViewModel @Inject constructor(
title = "Branch delete",
subtitle = "Deleting branch ${branch.simpleName}",
taskType = TaskType.DELETE_BRANCH,
positiveFeedbackText = "\"${branch.simpleName}\" deleted",
) { git ->
deleteBranchUseCase(git, branch)
positiveNotification("\"${branch.simpleName}\" deleted")
}
override fun checkoutRef(ref: Ref) = tabState.safeProcessing(
@ -54,9 +61,10 @@ class SharedBranchesViewModel @Inject constructor(
title = "Branch checkout",
subtitle = "Checking out branch ${ref.simpleName}",
taskType = TaskType.CHECKOUT_BRANCH,
positiveFeedbackText = "\"${ref.simpleName}\" checked out",
) { git ->
checkoutRefUseCase(git, ref)
positiveNotification("\"${ref.simpleName}\" checked out")
}
override fun rebaseBranch(ref: Ref) = tabState.safeProcessing(
@ -64,8 +72,10 @@ class SharedBranchesViewModel @Inject constructor(
title = "Branch rebase",
subtitle = "Rebasing branch ${ref.simpleName}",
taskType = TaskType.REBASE_BRANCH,
positiveFeedbackText = "\"${ref.simpleName}\" rebased",
refreshEvenIfCrashes = true,
) { git ->
rebaseBranchUseCase(git, ref)
positiveNotification("\"${ref.simpleName}\" rebased")
}
}

View File

@ -8,6 +8,7 @@ import com.jetpackduba.gitnuro.git.branches.CheckoutRefUseCase
import com.jetpackduba.gitnuro.git.remote_operations.DeleteRemoteBranchUseCase
import com.jetpackduba.gitnuro.git.remote_operations.PullFromSpecificBranchUseCase
import com.jetpackduba.gitnuro.git.remote_operations.PushToSpecificBranchUseCase
import com.jetpackduba.gitnuro.models.positiveNotification
import kotlinx.coroutines.Job
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
@ -31,17 +32,19 @@ class SharedRemotesViewModel @Inject constructor(
title = "Deleting remote branch",
subtitle = "Remote branch ${ref.simpleName} will be deleted from the remote",
taskType = TaskType.DELETE_REMOTE_BRANCH,
positiveFeedbackText = "Remote branch \"${ref.simpleName}\" deleted",
) { git ->
deleteRemoteBranchUseCase(git, ref)
positiveNotification("Remote branch \"${ref.simpleName}\" deleted",)
}
override fun checkoutRemoteBranch(remoteBranch: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
taskType = TaskType.CHECKOUT_REMOTE_BRANCH,
positiveFeedbackText = "\"${remoteBranch.simpleName}\" checked out",
) { git ->
checkoutRefUseCase(git, remoteBranch)
positiveNotification("\"${remoteBranch.simpleName}\" checked out")
}
override fun pushToRemoteBranch(branch: Ref) = tabState.safeProcessing(
@ -49,7 +52,6 @@ class SharedRemotesViewModel @Inject constructor(
title = "Push",
subtitle = "Pushing current branch to ${branch.simpleName}",
taskType = TaskType.PUSH_TO_BRANCH,
positiveFeedbackText = "Pushed to \"${branch.simpleName}\"",
) { git ->
pushToSpecificBranchUseCase(
git = git,
@ -57,6 +59,8 @@ class SharedRemotesViewModel @Inject constructor(
pushTags = false,
remoteBranch = branch,
)
positiveNotification("Pushed to \"${branch.simpleName}\"")
}
override fun pullFromRemoteBranch(branch: Ref) = tabState.safeProcessing(
@ -64,12 +68,13 @@ class SharedRemotesViewModel @Inject constructor(
title = "Pull",
subtitle = "Pulling changes from ${branch.simpleName} to the current branch",
taskType = TaskType.PULL_FROM_BRANCH,
positiveFeedbackText = "Pulled from \"${branch.simpleName}\"",
) { git ->
pullFromSpecificBranchUseCase(
git = git,
rebase = false,
remoteBranch = branch,
)
positiveNotification("Pulled from \"${branch.simpleName}\"")
}
}

View File

@ -6,6 +6,7 @@ import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.stash.ApplyStashUseCase
import com.jetpackduba.gitnuro.git.stash.DeleteStashUseCase
import com.jetpackduba.gitnuro.git.stash.PopStashUseCase
import com.jetpackduba.gitnuro.models.positiveNotification
import com.jetpackduba.gitnuro.ui.SelectedItem
import kotlinx.coroutines.Job
import org.eclipse.jgit.revwalk.RevCommit
@ -29,30 +30,33 @@ class SharedStashViewModel @Inject constructor(
refreshType = RefreshType.UNCOMMITTED_CHANGES_AND_LOG,
refreshEvenIfCrashes = true,
taskType = TaskType.APPLY_STASH,
positiveFeedbackText = "Stash applied",
) { git ->
applyStashUseCase(git, stashInfo)
positiveNotification("Stash applied")
}
override fun popStash(stash: RevCommit) = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITTED_CHANGES_AND_LOG,
refreshEvenIfCrashes = true,
taskType = TaskType.POP_STASH,
positiveFeedbackText = "Stash popped",
) { git ->
popStashUseCase(git, stash)
stashDropped(stash)
positiveNotification("Stash popped")
}
override fun deleteStash(stash: RevCommit) = tabState.safeProcessing(
refreshType = RefreshType.STASHES,
taskType = TaskType.DELETE_STASH,
positiveFeedbackText = "Stash deleted",
) { git ->
deleteStashUseCase(git, stash)
stashDropped(stash)
positiveNotification("Stash deleted")
}
override fun selectStash(stash: RevCommit) = tabState.runOperation(

View File

@ -5,6 +5,7 @@ import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.branches.CheckoutRefUseCase
import com.jetpackduba.gitnuro.models.positiveNotification
import kotlinx.coroutines.Job
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
@ -22,8 +23,9 @@ class SharedTagsViewModel @Inject constructor(
title = "Tag delete",
subtitle = "Deleting tag ${tag.simpleName}",
taskType = TaskType.DELETE_TAG,
positiveFeedbackText = "Tag \"${tag.simpleName}\" deleted",
) { git ->
deleteTagUseCase(git, tag)
positiveNotification("Tag \"${tag.simpleName}\" deleted")
}
}

View File

@ -18,6 +18,7 @@ import com.jetpackduba.gitnuro.git.rebase.SkipRebaseUseCase
import com.jetpackduba.gitnuro.git.repository.ResetRepositoryStateUseCase
import com.jetpackduba.gitnuro.git.workspace.*
import com.jetpackduba.gitnuro.models.AuthorInfo
import com.jetpackduba.gitnuro.models.positiveNotification
import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
import com.jetpackduba.gitnuro.ui.tree_files.TreeItem
import com.jetpackduba.gitnuro.ui.tree_files.entriesToTreeEntry
@ -216,17 +217,19 @@ class StatusViewModel @Inject constructor(
fun unstageAll() = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITTED_CHANGES,
taskType = TaskType.UNSTAGE_ALL_FILES,
positiveFeedbackText = null,
) { git ->
unstageAllUseCase(git)
null
}
fun stageAll() = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITTED_CHANGES,
taskType = TaskType.STAGE_ALL_FILES,
positiveFeedbackText = null,
) { git ->
stageAllUseCase(git)
null
}
fun resetStaged(statusEntry: StatusEntry) = tabState.runOperation(
@ -349,7 +352,6 @@ class StatusViewModel @Inject constructor(
fun commit(message: String) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
taskType = TaskType.DO_COMMIT,
positiveFeedbackText = if (isAmend.value) "Commit amended" else "New commit created",
) { git ->
val amend = isAmend.value
@ -364,6 +366,8 @@ class StatusViewModel @Inject constructor(
doCommitUseCase(git, commitMessage, amend, personIdent)
updateCommitMessage("")
_isAmend.value = false
positiveNotification(if (isAmend.value) "Commit amended" else "New commit created")
}
private suspend fun getPersonIdent(git: Git): PersonIdent? {
@ -404,7 +408,6 @@ class StatusViewModel @Inject constructor(
fun continueRebase(message: String) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
taskType = TaskType.CONTINUE_REBASE,
positiveFeedbackText = null,
) { git ->
val repositoryState = sharedRepositoryStateManager.repositoryState.value
val rebaseInteractiveState = sharedRepositoryStateManager.rebaseInteractiveState.value
@ -423,30 +426,35 @@ class StatusViewModel @Inject constructor(
}
continueRebaseUseCase(git)
null
}
fun abortRebase() = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
taskType = TaskType.ABORT_REBASE,
positiveFeedbackText = "Rebase aborted",
) { git ->
abortRebaseUseCase(git)
positiveNotification("Rebase aborted")
}
fun skipRebase() = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
taskType = TaskType.SKIP_REBASE,
positiveFeedbackText = null,
) { git ->
skipRebaseUseCase(git)
null
}
fun resetRepoState() = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
taskType = TaskType.RESET_REPO_STATE,
positiveFeedbackText = "Repository state has been reset",
) { git ->
resetRepositoryStateUseCase(git)
positiveNotification("Repository state has been reset")
}
fun deleteFile(statusEntry: StatusEntry) = tabState.runOperation(

View File

@ -20,6 +20,9 @@ import com.jetpackduba.gitnuro.managers.AppStateManager
import com.jetpackduba.gitnuro.managers.ErrorsManager
import com.jetpackduba.gitnuro.managers.newErrorNow
import com.jetpackduba.gitnuro.models.AuthorInfoSimple
import com.jetpackduba.gitnuro.models.errorNotification
import com.jetpackduba.gitnuro.models.positiveNotification
import com.jetpackduba.gitnuro.models.warningNotification
import com.jetpackduba.gitnuro.system.OpenFilePickerUseCase
import com.jetpackduba.gitnuro.system.OpenUrlInBrowserUseCase
import com.jetpackduba.gitnuro.system.PickerType
@ -353,7 +356,6 @@ class TabViewModel @Inject constructor(
fun blameFile(filePath: String) = tabState.safeProcessing(
refreshType = RefreshType.NONE,
taskType = TaskType.BLAME_FILE,
positiveFeedbackText = null,
) { git ->
_blameState.value = BlameState.Loading(filePath)
try {
@ -368,6 +370,8 @@ class TabViewModel @Inject constructor(
throw ex
}
null
}
fun resetBlameState() {
@ -418,18 +422,23 @@ class TabViewModel @Inject constructor(
refreshType = RefreshType.ALL_DATA,
refreshEvenIfCrashesInteractive = { it is CheckoutConflictException },
taskType = TaskType.CREATE_BRANCH,
positiveFeedbackText = "Branch \"${branchName}\" created",
) { git ->
createBranchUseCase(git, branchName)
positiveNotification("Branch \"${branchName}\" created")
}
fun stashWithMessage(message: String) = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITTED_CHANGES_AND_LOG,
taskType = TaskType.STASH,
positiveFeedbackText = "Changes stashed",
) { git ->
stageUntrackedFileUseCase(git)
stashChangesUseCase(git, message)
if (stashChangesUseCase(git, message)) {
positiveNotification("Changes stashed")
} else {
errorNotification("There are no changes to stash")
}
}
fun openFolderInFileExplorer() = tabState.runOperation(

View File

@ -11,6 +11,7 @@ import com.jetpackduba.gitnuro.git.branches.GetCurrentBranchUseCase
import com.jetpackduba.gitnuro.git.branches.GetRemoteBranchesUseCase
import com.jetpackduba.gitnuro.git.remotes.*
import com.jetpackduba.gitnuro.models.RemoteWrapper
import com.jetpackduba.gitnuro.models.positiveNotification
import com.jetpackduba.gitnuro.viewmodels.ISharedRemotesViewModel
import com.jetpackduba.gitnuro.viewmodels.SharedRemotesViewModel
import dagger.assisted.Assisted
@ -117,7 +118,6 @@ class RemotesViewModel @AssistedInject constructor(
fun deleteRemote(remoteName: String) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
taskType = TaskType.DELETE_REMOTE,
positiveFeedbackText = "Remote $remoteName deleted",
) { git ->
deleteRemoteUseCase(git, remoteName)
@ -129,6 +129,8 @@ class RemotesViewModel @AssistedInject constructor(
}
deleteLocallyRemoteBranchesUseCase(git, remoteToDeleteBranchesNames)
positiveNotification("Remote $remoteName deleted")
}

View File

@ -5,6 +5,7 @@ import com.jetpackduba.gitnuro.extensions.lowercaseContains
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.submodules.*
import com.jetpackduba.gitnuro.models.positiveNotification
import com.jetpackduba.gitnuro.ui.TabsManager
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -61,10 +62,11 @@ class SubmodulesViewModel @AssistedInject constructor(
fun initializeSubmodule(path: String) = tabState.safeProcessing(
refreshType = RefreshType.SUBMODULES,
taskType = TaskType.INIT_SUBMODULE,
positiveFeedbackText = null,
) { git ->
initializeSubmoduleUseCase(git, path)
updateSubmoduleUseCase(git, path)
null
}
suspend fun refresh(git: Git) {
@ -79,9 +81,10 @@ class SubmodulesViewModel @AssistedInject constructor(
refreshType = RefreshType.SUBMODULES,
title = "Deinitializing submodule $path",
taskType = TaskType.DEINIT_SUBMODULE,
positiveFeedbackText = null,
) { git ->
deInitializeSubmoduleUseCase(git, path)
null
}
fun onSyncSubmodule(path: String) = tabState.safeProcessing(
@ -89,9 +92,10 @@ class SubmodulesViewModel @AssistedInject constructor(
title = "Syncing submodule $path",
subtitle = "Please wait until synchronization has finished",
taskType = TaskType.SYNC_SUBMODULE,
positiveFeedbackText = "Submodule synced",
) { git ->
syncSubmoduleUseCase(git, path)
positiveNotification("Submodule synced")
}
fun onUpdateSubmodule(path: String) = tabState.safeProcessing(
@ -99,15 +103,15 @@ class SubmodulesViewModel @AssistedInject constructor(
title = "Updating submodule $path",
subtitle = "Please wait until update has finished",
taskType = TaskType.UPDATE_SUBMODULE,
positiveFeedbackText = "Submodule updated",
) { git ->
updateSubmoduleUseCase(git, path)
positiveNotification("Submodule updated")
}
fun onCreateSubmodule(repository: String, directory: String) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
taskType = TaskType.ADD_SUBMODULE,
positiveFeedbackText = "Submodule created",
) { git ->
addSubmoduleUseCase(
git = git,
@ -115,14 +119,17 @@ class SubmodulesViewModel @AssistedInject constructor(
path = directory,
uri = repository,
)
positiveNotification("Submodule created")
}
fun onDeleteSubmodule(path: String) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
taskType = TaskType.DELETE_SUBMODULE,
positiveFeedbackText = "Submodule deleted",
) { git ->
deleteSubmoduleUseCase(git, path)
positiveNotification("Submodule deleted")
}
}

View File

@ -8,6 +8,7 @@ import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.log.CheckoutCommitUseCase
import com.jetpackduba.gitnuro.git.tags.DeleteTagUseCase
import com.jetpackduba.gitnuro.git.tags.GetTagsUseCase
import com.jetpackduba.gitnuro.models.positiveNotification
import com.jetpackduba.gitnuro.viewmodels.ISharedTagsViewModel
import com.jetpackduba.gitnuro.viewmodels.SharedTagsViewModel
import dagger.assisted.Assisted
@ -60,9 +61,10 @@ class TagsViewModel @AssistedInject constructor(
fun checkoutTagCommit(ref: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
taskType = TaskType.INIT_SUBMODULE,
positiveFeedbackText = "Commit checked out",
) { git ->
checkoutCommitUseCase(git, ref.objectId.name)
positiveNotification("Commit checked out")
}
fun selectTag(tag: Ref) {