Compare commits

..

1 Commits

Author SHA1 Message Date
Abdelilah El Aissaoui
b1522fdee6
Added keybindings labels 2024-09-17 23:40:32 +02:00
24 changed files with 369 additions and 233 deletions

View File

@ -8,8 +8,6 @@
environment,
please read [its documentation](https://www.rust-lang.org/). `cargo` and `rustc` must be available in the path in
order to build Gitnuro properly.
- **cargo-kotars:** A tool to run autogenerated bindings from Rust to Kotlin. You can install it using
`cargo install cargo-kotars --git https://github.com/JetpackDuba/kotars`
- **Perl:** Perl is required to build openssl (which is required for LibSSH to work).
- **Packages for Linux ARM64/aarch64**: You need to install the `aarch64-linux-gnu-gcc` package to cross compile the
Rust components to ARM from x86_64. You will also need to use `rustup` to add a new

View File

@ -12,17 +12,17 @@ plugins {
kotlin("jvm") version "2.0.0"
kotlin("plugin.serialization") version "2.0.20"
id("com.google.devtools.ksp") version "2.0.20-1.0.24"
id("org.jetbrains.compose") version "1.7.0"
id("org.jetbrains.compose") version "1.7.0-beta02"
id("org.jetbrains.kotlin.plugin.compose") version "2.0.20"
}
// Remember to update Constants.APP_VERSION when changing this version
val projectVersion = "1.4.1"
val projectVersion = "1.4.0-rc01"
val projectName = "Gitnuro"
// Required for JPackage, as it doesn't accept additional suffixes after the version.
val projectVersionSimplified = "1.4.1"
val projectVersionSimplified = "1.4.0"
val rustGeneratedSource = "${layout.buildDirectory.get()}/generated/source/uniffi/main/com/jetpackduba/gitnuro/java"

View File

@ -1,5 +1,5 @@
{
"appVersion": "1.4.0",
"appCode": 14,
"downloadUrl": "https://github.com/JetpackDuba/Gitnuro/releases/tag/v1.4.0"
"appVersion": "1.3.1",
"appCode": 11,
"downloadUrl": "https://github.com/JetpackDuba/Gitnuro/releases/tag/v1.3.1"
}

View File

@ -277,6 +277,7 @@ class App {
modifier = Modifier
.background(MaterialTheme.colors.background)
.onPreviewKeyEvent {
println(it.toString())
when {
it.matchesBinding(KeybindingOption.OPEN_NEW_TAB) -> {
tabsManager.addNewEmptyTab()

View File

@ -22,8 +22,8 @@ object AppConstants {
const val APP_NAME = "Gitnuro"
const val APP_DESCRIPTION =
"Gitnuro is a Git client that allows you to manage multiple repositories with a modern experience and live visual representation of your repositories' state."
const val APP_VERSION = "1.4.1"
const val APP_VERSION_CODE = 15
const val APP_VERSION = "1.4.0-rc01"
const val APP_VERSION_CODE = 13
const val VERSION_CHECK_URL = "https://raw.githubusercontent.com/JetpackDuba/Gitnuro/main/latest.json"
}

View File

@ -2,7 +2,6 @@ package com.jetpackduba.gitnuro.credentials
import com.jetpackduba.gitnuro.exceptions.NotSupportedHelper
import com.jetpackduba.gitnuro.git.remote_operations.CredentialsCache
import com.jetpackduba.gitnuro.logging.printError
import com.jetpackduba.gitnuro.logging.printLog
import com.jetpackduba.gitnuro.managers.IShellManager
import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
@ -260,8 +259,7 @@ class HttpCredentialsProvider @AssistedInject constructor(
val credentialHelperPath = uriSpecificCredentialHelper ?: genericCredentialHelper ?: return null
if (credentialHelperPath == "cache" || credentialHelperPath == "store") {
printError(TAG, "Invalid credentials helper: \"$credentialHelperPath\" is not yet supported")
return null
throw NotSupportedHelper("Invalid credentials helper: \"$credentialHelperPath\" is not yet supported")
}
// TODO Try to use "git-credential-manager-core" when "manager-core" is detected. Works for linux but requires testing for mac/windows

View File

@ -1,6 +1,11 @@
package com.jetpackduba.gitnuro.exceptions
fun codeToMessage(code: Int): String {
class WatcherInitException(
code: Int,
message: String = codeToMessage(code),
) : GitnuroException(message)
private fun codeToMessage(code: Int): String {
return when (code) {
1 /*is WatcherInitException.Generic*/, 2 /*is WatcherInitException.Io*/ -> "Could not watch directory. Check if it exists and you have read permissions."
3 /*is WatcherInitException.PathNotFound*/ -> "Path not found, check if your repository still exists"

View File

@ -3,6 +3,7 @@ package com.jetpackduba.gitnuro.git
import FileWatcher
import WatchDirectoryNotifier
import com.jetpackduba.gitnuro.di.TabScope
import com.jetpackduba.gitnuro.exceptions.WatcherInitException
import com.jetpackduba.gitnuro.git.workspace.GetIgnoreRulesUseCase
import com.jetpackduba.gitnuro.system.systemSeparator
import kotlinx.coroutines.CoroutineScope
@ -24,8 +25,8 @@ class FileChangesWatcher @Inject constructor(
private val getIgnoreRulesUseCase: GetIgnoreRulesUseCase,
private val tabScope: CoroutineScope,
) : AutoCloseable {
private val _changesNotifier = MutableSharedFlow<WatcherEvent>()
val changesNotifier: SharedFlow<WatcherEvent> = _changesNotifier
private val _changesNotifier = MutableSharedFlow<Boolean>()
val changesNotifier: SharedFlow<Boolean> = _changesNotifier
private val fileWatcher = FileWatcher.new()
private var shouldKeepLooping = true
@ -70,16 +71,14 @@ class FileChangesWatcher @Inject constructor(
if (!areAllPathsIgnored) {
println("Emitting changes $hasGitIgnoreChanged")
_changesNotifier.emit(WatcherEvent.RepositoryChanged(hasGitDirChanged))
_changesNotifier.emit(hasGitDirChanged)
}
}
}
override fun onError(code: Int) {
tabScope.launch {
_changesNotifier.emit(WatcherEvent.WatchInitError(code))
}
throw WatcherInitException(code)
}
}
@ -91,9 +90,4 @@ class FileChangesWatcher @Inject constructor(
fileWatcher.close()
}
}
sealed interface WatcherEvent {
data class RepositoryChanged(val hasGitDirChanged: Boolean) : WatcherEvent
data class WatchInitError(val code: Int) : WatcherEvent
}

View File

@ -15,12 +15,9 @@ class SaveAuthorUseCase @Inject constructor() {
repoConfig.load()
if (globalConfig is FileBasedConfig) {
val canonicalConfigFile = globalConfig.file.canonicalFile
val globalRepoConfig = FileBasedConfig(canonicalConfigFile, git.repository.fs)
globalRepoConfig.setStringProperty("user", null, "name", newAuthorInfo.globalName)
globalRepoConfig.setStringProperty("user", null, "email", newAuthorInfo.globalEmail)
globalRepoConfig.save()
globalConfig.setStringProperty("user", null, "name", newAuthorInfo.globalName)
globalConfig.setStringProperty("user", null, "email", newAuthorInfo.globalEmail)
globalConfig.save()
}
config.setStringProperty("user", null, "name", newAuthorInfo.name)

View File

@ -37,7 +37,8 @@ class GetLogUseCase @Inject constructor() {
if (hasUncommittedChanges)
commitList.addUncommittedChangesGraphCommit(logList.first())
// val count = walk.count()
// println("Commits list count is $count")
commitList.source(walk)
commitList.fillTo(commitsLimit)
}

View File

@ -1,34 +0,0 @@
package com.jetpackduba.gitnuro.git.remote_operations
import org.eclipse.jgit.api.MergeResult
import org.eclipse.jgit.api.PullResult
import org.eclipse.jgit.api.RebaseResult
import javax.inject.Inject
typealias PullHasConflicts = Boolean
class HasPullResultConflictsUseCase @Inject constructor() {
operator fun invoke(isRebase: Boolean, pullResult: PullResult): PullHasConflicts {
if (!pullResult.isSuccessful) {
if (
pullResult.mergeResult?.mergeStatus == MergeResult.MergeStatus.CONFLICTING ||
pullResult.rebaseResult?.status == RebaseResult.Status.CONFLICTS ||
pullResult.rebaseResult?.status == RebaseResult.Status.STOPPED
) {
return true
}
if (isRebase) {
val message = when (pullResult.rebaseResult.status) {
RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommitted changes"
else -> "Pull failed"
}
throw Exception(message)
}
}
return false
}
}

View File

@ -4,15 +4,15 @@ import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.transport.CredentialsProvider
import javax.inject.Inject
class PullBranchUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase,
private val appSettingsRepository: AppSettingsRepository,
private val hasPullResultConflictsUseCase: HasPullResultConflictsUseCase,
) {
suspend operator fun invoke(git: Git, pullType: PullType): PullHasConflicts = withContext(Dispatchers.IO) {
suspend operator fun invoke(git: Git, pullType: PullType) = withContext(Dispatchers.IO) {
val pullWithRebase = when (pullType) {
PullType.REBASE -> true
PullType.MERGE -> false
@ -27,7 +27,19 @@ class PullBranchUseCase @Inject constructor(
.setCredentialsProvider(CredentialsProvider.getDefault())
.call()
return@handleTransportUseCase hasPullResultConflictsUseCase(pullWithRebase, pullResult)
if (!pullResult.isSuccessful) {
var message = "Pull failed"
if (pullWithRebase) {
message = when (pullResult.rebaseResult.status) {
RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommitted changes"
RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue"
else -> message
}
}
throw Exception(message)
}
}
}
}

View File

@ -2,34 +2,42 @@ package com.jetpackduba.gitnuro.git.remote_operations
import com.jetpackduba.gitnuro.extensions.remoteName
import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.transport.CredentialsProvider
import javax.inject.Inject
class PullFromSpecificBranchUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase,
private val hasPullResultConflictsUseCase: HasPullResultConflictsUseCase,
private val appSettingsRepository: AppSettingsRepository,
) {
suspend operator fun invoke(git: Git, remoteBranch: Ref): PullHasConflicts =
withContext(Dispatchers.IO) {
val pullWithRebase = appSettingsRepository.pullRebase
suspend operator fun invoke(git: Git, rebase: Boolean, remoteBranch: Ref) = withContext(Dispatchers.IO) {
handleTransportUseCase(git) {
val pullResult = git
.pull()
.setTransportConfigCallback { handleTransport(it) }
.setRemote(remoteBranch.remoteName)
.setRemoteBranchName(remoteBranch.simpleName)
.setRebase(rebase)
.setCredentialsProvider(CredentialsProvider.getDefault())
.call()
handleTransportUseCase(git) {
val pullResult = git
.pull()
.setTransportConfigCallback { handleTransport(it) }
.setRemote(remoteBranch.remoteName)
.setRemoteBranchName(remoteBranch.simpleName)
.setRebase(pullWithRebase)
.setCredentialsProvider(CredentialsProvider.getDefault())
.call()
if (!pullResult.isSuccessful) {
var message =
"Pull failed" // TODO Remove messages from here and pass the result to a custom exception type
return@handleTransportUseCase hasPullResultConflictsUseCase(pullWithRebase, pullResult)
if (rebase) {
message = when (pullResult.rebaseResult.status) {
RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommitted changes"
RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue"
else -> message
}
}
throw Exception(message)
}
}
}
}

View File

@ -142,9 +142,11 @@ private fun baseKeybindings() = mapOf(
),
KeybindingOption.CHANGE_CURRENT_TAB_LEFT to listOf(
Keybinding(key = Key.DirectionLeft, alt = true),
Keybinding(key = Key.Tab, control = true, shift = true),
),
KeybindingOption.CHANGE_CURRENT_TAB_RIGHT to listOf(
Keybinding(key = Key.DirectionRight, alt = true),
Keybinding(key = Key.Tab, control = true),
),
)
@ -155,10 +157,26 @@ private fun macKeybindings(): Map<KeybindingOption, List<Keybinding>> {
val macBindings = baseKeybindings().toMutableMap()
macBindings.apply {
this[KeybindingOption.REFRESH] = listOf(
Keybinding(key = Key.F5),
Keybinding(meta = true, key = Key.R),
val keysToReplaceControlWithCommand = listOf(
KeybindingOption.REFRESH,
KeybindingOption.PULL,
KeybindingOption.PUSH,
KeybindingOption.BRANCH_CREATE,
KeybindingOption.STASH,
KeybindingOption.STASH_POP,
KeybindingOption.OPEN_REPOSITORY,
KeybindingOption.OPEN_NEW_TAB,
KeybindingOption.CLOSE_CURRENT_TAB,
)
for (key in keysToReplaceControlWithCommand) {
val originalKeybindings = this[key] ?: emptyList()
val newKeybindings = originalKeybindings.map {
it.copy(meta = it.control, control = false)
}
this[key] = newKeybindings
}
}
return macBindings
@ -185,4 +203,7 @@ fun KeyEvent.matchesBinding(keybindingOption: KeybindingOption): Boolean {
keybinding.shift == this.isShiftPressed &&
keybinding.key == this.key
} && this.type == KeyEventType.KeyDown
}
}
val KeybindingOption.keyBinding
get() = keybindings[this]?.firstOrNull()

View File

@ -2,6 +2,7 @@
package com.jetpackduba.gitnuro.ui
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -20,10 +21,12 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup
@ -34,6 +37,11 @@ import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.extensions.handOnHover
import com.jetpackduba.gitnuro.extensions.ignoreKeyEvents
import com.jetpackduba.gitnuro.git.remote_operations.PullType
import com.jetpackduba.gitnuro.keybindings.Keybinding
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.keyBinding
import com.jetpackduba.gitnuro.theme.notoSansMonoFontFamily
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.ui.components.PrimaryButton
import com.jetpackduba.gitnuro.ui.components.tooltip.InstantTooltip
import com.jetpackduba.gitnuro.ui.context_menu.*
@ -62,19 +70,17 @@ fun Menu(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
InstantTooltip(
text = "Open a different repository",
enabled = !showOpenPopup,
) {
MenuButton(
modifier = Modifier
.padding(start = 16.dp)
.onGloballyPositioned { setPosition(it) },
title = "Open",
icon = painterResource(AppIcons.OPEN),
onClick = { onShowOpenPopupChange(true) },
)
}
MenuButton(
modifier = Modifier
.padding(start = 16.dp)
.onGloballyPositioned { setPosition(it) },
title = "Open",
icon = painterResource(AppIcons.OPEN),
keybinding = KeybindingOption.OPEN_REPOSITORY.keyBinding,
tooltip = "Open a different repository",
tooltipEnabled = !showOpenPopup,
onClick = { onShowOpenPopupChange(true) },
)
Spacer(modifier = Modifier.weight(1f))
@ -126,16 +132,15 @@ fun Menu(
Spacer(modifier = Modifier.width(32.dp))
InstantTooltip(
text = "Create a new branch",
) {
MenuButton(
title = "Branch",
icon = painterResource(AppIcons.BRANCH),
) {
MenuButton(
title = "Branch",
icon = painterResource(AppIcons.BRANCH),
onClick = {
onCreateBranch()
}
}
},
tooltip = "Create a new branch",
keybinding = KeybindingOption.BRANCH_CREATE.keyBinding,
)
Spacer(modifier = Modifier.width(32.dp))
@ -151,43 +156,42 @@ fun Menu(
)
)
InstantTooltip(
text = "Pop the last stash"
) {
MenuButton(
title = "Pop",
icon = painterResource(AppIcons.APPLY_STASH),
) { menuViewModel.popStash() }
}
MenuButton(
title = "Pop",
icon = painterResource(AppIcons.APPLY_STASH),
keybinding = KeybindingOption.STASH_POP.keyBinding,
tooltip = "Pop the last stash",
) { menuViewModel.popStash() }
Spacer(modifier = Modifier.weight(1f))
InstantTooltip(
text = "Open a terminal in the repository's path"
) {
MenuButton(
modifier = Modifier.padding(end = 4.dp),
title = "Terminal",
icon = painterResource(AppIcons.TERMINAL),
onClick = { menuViewModel.openTerminal() },
)
}
MenuButton(
modifier = Modifier.padding(end = 4.dp),
title = "Terminal",
icon = painterResource(AppIcons.TERMINAL),
onClick = { menuViewModel.openTerminal() },
tooltip = "Open a terminal in the repository's path",
keybinding = null,
)
MenuButton(
modifier = Modifier.padding(end = 4.dp),
title = "Actions",
icon = painterResource(AppIcons.BOLT),
onClick = onQuickActions,
tooltip = "Additional actions",
keybinding = KeybindingOption.STASH_POP.keyBinding,
)
InstantTooltip(
text = "Gitnuro's settings",
Box(
modifier = Modifier.padding(end = 16.dp)
) {
MenuButton(
title = "Settings",
icon = painterResource(AppIcons.SETTINGS),
onClick = onShowSettingsDialog,
tooltip = "Gitnuro's settings",
keybinding = KeybindingOption.STASH_POP.keyBinding,
)
}
}
@ -257,35 +261,137 @@ fun MenuButton(
enabled: Boolean = true,
title: String,
icon: Painter,
onClick: () -> Unit
keybinding: Keybinding?,
tooltip: String,
tooltipEnabled: Boolean = true,
onClick: () -> Unit,
) {
Column(
modifier = modifier
.ignoreKeyEvents()
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colors.surface)
.handMouseClickable { if (enabled) onClick() }
.size(56.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
InstantTooltip(
text = tooltip,
enabled = tooltipEnabled,
trailingContent = if (keybinding != null) {
{ KeybindingHint(keybinding) }
} else {
null
}
) {
Icon(
painter = icon,
contentDescription = title,
modifier = Modifier
.size(24.dp),
tint = MaterialTheme.colors.onBackground,
)
Text(
text = title,
style = MaterialTheme.typography.caption,
maxLines = 1,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onBackground,
)
Column(
modifier = modifier
.ignoreKeyEvents()
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colors.surface)
.handMouseClickable { if (enabled) onClick() }
.size(56.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Icon(
painter = icon,
contentDescription = title,
modifier = Modifier
.size(24.dp),
tint = MaterialTheme.colors.onBackground,
)
Text(
text = title,
style = MaterialTheme.typography.caption,
maxLines = 1,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onBackground,
)
}
}
}
@Composable
fun KeybindingHint(keybinding: Keybinding) {
val parts = remember(keybinding) { getParts(keybinding) }.joinToString("+")
Text(
parts,
fontFamily = notoSansMonoFontFamily,
fontSize = MaterialTheme.typography.caption.fontSize,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.onBackgroundSecondary,
)
}
@Preview
@Composable
fun KeybindingHintPartPreview() {
KeybindingHintPart("CTRL")
}
@Composable
fun KeybindingHintPart(part: String) {
Text(
text = part,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.primary,
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.border(2.dp, MaterialTheme.colors.primary, RoundedCornerShape(4.dp))
.background(MaterialTheme.colors.primary.copy(alpha = 0.05f))
.padding(horizontal = 4.dp, vertical = 4.dp)
)
}
fun getParts(keybinding: Keybinding): List<String> {
val parts = mutableListOf<String>()
if (keybinding.control) {
parts.add("Ctrl")
}
if (keybinding.meta) {
parts.add("")
}
if (keybinding.alt) {
parts.add("Alt")
}
if (keybinding.shift) {
parts.add("Shift")
}
val key = when (keybinding.key) {
Key.A -> "A"
Key.B -> "B"
Key.C -> "C"
Key.D -> "D"
Key.E -> "E"
Key.F -> "F"
Key.G -> "G"
Key.H -> "H"
Key.I -> "I"
Key.J -> "J"
Key.K -> "K"
Key.L -> "L"
Key.M -> "M"
Key.N -> "N"
Key.O -> "O"
Key.P -> "P"
Key.Q -> "Q"
Key.R -> "R"
Key.S -> "S"
Key.T -> "T"
Key.U -> "U"
Key.V -> "V"
Key.W -> "W"
Key.X -> "X"
Key.Y -> "Y"
Key.Z -> "Z"
Key.Tab -> "Tab"
else -> throw NotImplementedError("Key not implemented")
}
parts.add(key)
return parts
}
@Composable
fun ExtendedMenuButton(
modifier: Modifier = Modifier,

View File

@ -89,6 +89,7 @@ fun UncommittedChanges(
val doCommit = {
statusViewModel.commit(commitMessage)
onStagedDiffEntrySelected(null)
setCommitMessage("")
}
val canCommit = commitMessage.isNotEmpty() && stageStateUi.hasStagedFiles

View File

@ -28,6 +28,7 @@ import com.jetpackduba.gitnuro.theme.isDark
@Composable
fun InstantTooltip(
text: String?,
trailingContent: (@Composable () -> Unit)? = null,
modifier: Modifier = Modifier,
position: InstantTooltipPosition = InstantTooltipPosition.BOTTOM,
enabled: Boolean = true,
@ -71,14 +72,14 @@ fun InstantTooltip(
onDismissRequest = {}
) {
val padding = when(position) {
val padding = when (position) {
InstantTooltipPosition.TOP -> PaddingValues(bottom = 4.dp)
InstantTooltipPosition.BOTTOM -> PaddingValues(top = 4.dp)
InstantTooltipPosition.LEFT -> PaddingValues(end = 4.dp)
InstantTooltipPosition.RIGHT -> PaddingValues(start = 4.dp)
}
Box(
Row(
modifier = Modifier
.padding(padding)
.shadow(8.dp)
@ -94,15 +95,21 @@ fun InstantTooltip(
)
} else
this
},
}
.padding(8.dp),
) {
Text(
text = text,
fontSize = 12.sp,
maxLines = 1,
color = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(8.dp)
color = MaterialTheme.colors.onBackground
)
if (trailingContent != null) {
Spacer(Modifier.width(8.dp))
trailingContent()
}
}
}
}

View File

@ -32,9 +32,7 @@ fun ErrorDialog(
val clipboard = LocalClipboardManager.current
var showStackTrace by remember { mutableStateOf(false) }
MaterialDialog (
onCloseRequested = onAccept,
) {
MaterialDialog {
Column(
modifier = Modifier
.width(580.dp)

View File

@ -9,9 +9,10 @@ import com.jetpackduba.gitnuro.git.remote_operations.PullType
import com.jetpackduba.gitnuro.git.remote_operations.PushBranchUseCase
import com.jetpackduba.gitnuro.git.stash.PopLastStashUseCase
import com.jetpackduba.gitnuro.git.stash.StashChangesUseCase
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 kotlinx.coroutines.Job
import javax.inject.Inject
@ -25,7 +26,7 @@ interface IGlobalMenuActionsViewModel {
fun openTerminal(): Job
}
class GlobalMenuActionsViewModel @Inject constructor(
class GlobalMenuActionsViewModel @Inject constructor(
private val tabState: TabState,
private val pullBranchUseCase: PullBranchUseCase,
private val pushBranchUseCase: PushBranchUseCase,
@ -33,6 +34,8 @@ class GlobalMenuActionsViewModel @Inject constructor(
private val popLastStashUseCase: PopLastStashUseCase,
private val stashChangesUseCase: StashChangesUseCase,
private val openRepositoryInTerminalUseCase: OpenRepositoryInTerminalUseCase,
settings: AppSettingsRepository,
appStateManager: AppStateManager,
) : IGlobalMenuActionsViewModel {
override fun pull(pullType: PullType) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
@ -41,11 +44,9 @@ class GlobalMenuActionsViewModel @Inject constructor(
refreshEvenIfCrashes = true,
taskType = TaskType.PULL,
) { git ->
if (pullBranchUseCase(git, pullType)) {
warningNotification("Pull produced conflicts, fix them to continue")
} else {
positiveNotification("Pull completed")
}
pullBranchUseCase(git, pullType)
positiveNotification("Pull completed")
}
override fun fetchAll() = tabState.safeProcessing(

View File

@ -1,10 +1,25 @@
package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.TaskType
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.remote_operations.FetchAllRemotesUseCase
import com.jetpackduba.gitnuro.git.remote_operations.PullBranchUseCase
import com.jetpackduba.gitnuro.git.remote_operations.PullType
import com.jetpackduba.gitnuro.git.remote_operations.PushBranchUseCase
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
class MenuViewModel @Inject constructor(
private val tabState: TabState,
private val globalMenuActionsViewModel: GlobalMenuActionsViewModel,
settings: AppSettingsRepository,
appStateManager: AppStateManager,

View File

@ -2,7 +2,7 @@ package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.SharedRepositoryStateManager
import com.jetpackduba.gitnuro.TaskType
import com.jetpackduba.gitnuro.exceptions.codeToMessage
import com.jetpackduba.gitnuro.exceptions.WatcherInitException
import com.jetpackduba.gitnuro.git.*
import com.jetpackduba.gitnuro.git.branches.CreateBranchUseCase
import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState
@ -110,8 +110,6 @@ class RepositoryOpenViewModel @Inject constructor(
var authorViewModel: AuthorViewModel? = null
private set
private var hasGitDirChanged = false
init {
tabScope.run {
launch {
@ -121,7 +119,7 @@ class RepositoryOpenViewModel @Inject constructor(
}
launch {
watchRepositoryChanges()
watchRepositoryChanges(tabState.git)
}
}
}
@ -160,53 +158,55 @@ class RepositoryOpenViewModel @Inject constructor(
* the app by constantly running "git status" or even full refreshes.
*
*/
private suspend fun watchRepositoryChanges() = tabScope.launch(Dispatchers.IO) {
launch {
fileChangesWatcher.changesNotifier.collect { watcherEvent ->
when (watcherEvent) {
is WatcherEvent.RepositoryChanged -> repositoryChanged(watcherEvent.hasGitDirChanged)
is WatcherEvent.WatchInitError -> {
val message = codeToMessage(watcherEvent.code)
errorsManager.addError(
newErrorNow(
exception = Exception(message),
taskType = TaskType.CHANGES_DETECTION,
),
)
private suspend fun watchRepositoryChanges(git: Git) = tabScope.launch(Dispatchers.IO) {
var hasGitDirChanged = false
launch {
fileChangesWatcher.changesNotifier.collect { latestUpdateChangedGitDir ->
val isOperationRunning = tabState.operationRunning
if (!isOperationRunning) { // Only update if there isn't any process running
printDebug(TAG, "Detected changes in the repository's directory")
val currentTimeMillis = System.currentTimeMillis()
if (
latestUpdateChangedGitDir &&
currentTimeMillis - tabState.lastOperation < MIN_TIME_AFTER_GIT_OPERATION
) {
printDebug(TAG, "Git operation was executed recently, ignoring file system change")
return@collect
}
if (latestUpdateChangedGitDir) {
hasGitDirChanged = true
}
if (isActive) {
updateApp(hasGitDirChanged)
}
hasGitDirChanged = false
} else {
printDebug(TAG, "Ignored file events during operation")
}
}
}
}
private suspend fun CoroutineScope.repositoryChanged(hasGitDirChanged: Boolean) {
val isOperationRunning = tabState.operationRunning
if (!isOperationRunning) { // Only update if there isn't any process running
printDebug(TAG, "Detected changes in the repository's directory")
val currentTimeMillis = System.currentTimeMillis()
if (
hasGitDirChanged &&
currentTimeMillis - tabState.lastOperation < MIN_TIME_AFTER_GIT_OPERATION
) {
printDebug(TAG, "Git operation was executed recently, ignoring file system change")
return
try {
fileChangesWatcher.watchDirectoryPath(
repository = git.repository,
)
} catch (ex: WatcherInitException) {
val message = ex.message
if (message != null) {
errorsManager.addError(
newErrorNow(
exception = ex,
taskType = TaskType.CHANGES_DETECTION,
),
)
}
if (hasGitDirChanged) {
this@RepositoryOpenViewModel.hasGitDirChanged = true
}
if (isActive) {
updateApp(hasGitDirChanged)
}
this@RepositoryOpenViewModel.hasGitDirChanged = false
} else {
printDebug(TAG, "Ignored file events during operation")
}
}

View File

@ -9,7 +9,6 @@ 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 com.jetpackduba.gitnuro.models.warningNotification
import kotlinx.coroutines.Job
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
@ -70,10 +69,12 @@ class SharedRemotesViewModel @Inject constructor(
subtitle = "Pulling changes from ${branch.simpleName} to the current branch",
taskType = TaskType.PULL_FROM_BRANCH,
) { git ->
if (pullFromSpecificBranchUseCase(git = git, remoteBranch = branch)) {
warningNotification("Pull produced conflicts, fix them to continue")
} else {
positiveNotification("Pulled from \"${branch.simpleName}\"")
}
pullFromSpecificBranchUseCase(
git = git,
rebase = false,
remoteBranch = branch,
)
positiveNotification("Pulled from \"${branch.simpleName}\"")
}
}

View File

@ -395,9 +395,7 @@ class StatusViewModel @Inject constructor(
val personIdent = getPersonIdent(git)
doCommitUseCase(git, commitMessage, amend, personIdent)
updateCommitMessage("")
_commitMessageChangesFlow.emit("")
_isAmend.value = false
positiveNotification(if (isAmend.value) "Commit amended" else "New commit created")

View File

@ -1,37 +1,45 @@
package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.SharedRepositoryStateManager
import com.jetpackduba.gitnuro.TaskType
import com.jetpackduba.gitnuro.credentials.CredentialsAccepted
import com.jetpackduba.gitnuro.credentials.CredentialsState
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager
import com.jetpackduba.gitnuro.git.FileChangesWatcher
import com.jetpackduba.gitnuro.git.ProcessingState
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.exceptions.WatcherInitException
import com.jetpackduba.gitnuro.git.*
import com.jetpackduba.gitnuro.git.branches.CreateBranchUseCase
import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState
import com.jetpackduba.gitnuro.git.repository.InitLocalRepositoryUseCase
import com.jetpackduba.gitnuro.git.repository.OpenRepositoryUseCase
import com.jetpackduba.gitnuro.git.repository.OpenSubmoduleRepositoryUseCase
import com.jetpackduba.gitnuro.git.stash.StashChangesUseCase
import com.jetpackduba.gitnuro.git.workspace.StageUntrackedFileUseCase
import com.jetpackduba.gitnuro.logging.printDebug
import com.jetpackduba.gitnuro.logging.printLog
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.system.OpenFilePickerUseCase
import com.jetpackduba.gitnuro.system.OpenUrlInBrowserUseCase
import com.jetpackduba.gitnuro.system.PickerType
import com.jetpackduba.gitnuro.ui.IVerticalSplitPaneConfig
import com.jetpackduba.gitnuro.ui.SelectedItem
import com.jetpackduba.gitnuro.ui.TabsManager
import com.jetpackduba.gitnuro.ui.VerticalSplitPaneConfig
import com.jetpackduba.gitnuro.updates.Update
import com.jetpackduba.gitnuro.updates.UpdatesRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.errors.CheckoutConflictException
import org.eclipse.jgit.blame.BlameResult
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.lib.RepositoryState
import org.eclipse.jgit.revwalk.RevCommit
import java.awt.Desktop
import java.io.File
import javax.inject.Inject
import javax.inject.Provider