Compare commits

..

12 Commits

Author SHA1 Message Date
Abdelilah El Aissaoui
edcefd7a38 Changed version to 1.4.1 2024-11-03 13:23:32 +01:00
Abdelilah El Aissaoui
01b0827057 Fixed commit message disappear if pre-commit hook fails
Fixes #248
2024-11-02 23:53:07 +01:00
Abdelilah El Aissaoui
db1467c354
Having "cache" or "store" in credentials manager config does not longer throw an error 2024-10-31 01:37:49 +01:00
Abdelilah El Aissaoui
07e98dae10
Errors dialog can now be closed with ESC button 2024-10-31 01:37:22 +01:00
Abdelilah El Aissaoui
644f33ff2c
Fixed gitconfig not updating symlink origin instead of replacing symlink file
Fixes #245
2024-10-31 01:36:22 +01:00
Abdelilah El Aissaoui
3128341b93
Fixed crash when watch init has failed 2024-10-22 21:15:56 +02:00
Abdelilah El Aissaoui
1ef596ad3c
Updated latest.json for v1.4.0 2024-10-06 02:44:32 +02:00
Abdelilah El Aissaoui
4cc87a7289
Bumped version to 1.4.0 2024-10-01 20:35:09 +02:00
Abdelilah El Aissaoui
fcecd0380a
Merge pull request #239 from grizeldi/patch-1
Mention cargo-kotars in the development guide
2024-09-30 21:37:01 +02:00
grizeldi
fc2098781b Added automatic cargo-kotars install command to documentation 2024-09-28 20:21:05 +02:00
grizeldi
ce694ed44b
Mention cargo-kotars in the development guide 2024-09-27 23:32:20 +02:00
Abdelilah El Aissaoui
e5899d02d6
Fixed pull result not being shown properly
Also fixed pull from specific branch using  merge by default instead of the user's configuration
2024-09-19 00:29:53 +02:00
24 changed files with 232 additions and 368 deletions

View File

@ -8,6 +8,8 @@
environment, environment,
please read [its documentation](https://www.rust-lang.org/). `cargo` and `rustc` must be available in the path in please read [its documentation](https://www.rust-lang.org/). `cargo` and `rustc` must be available in the path in
order to build Gitnuro properly. 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). - **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 - **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 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("jvm") version "2.0.0"
kotlin("plugin.serialization") version "2.0.20" kotlin("plugin.serialization") version "2.0.20"
id("com.google.devtools.ksp") version "2.0.20-1.0.24" id("com.google.devtools.ksp") version "2.0.20-1.0.24"
id("org.jetbrains.compose") version "1.7.0-beta02" id("org.jetbrains.compose") version "1.7.0"
id("org.jetbrains.kotlin.plugin.compose") version "2.0.20" id("org.jetbrains.kotlin.plugin.compose") version "2.0.20"
} }
// Remember to update Constants.APP_VERSION when changing this version // Remember to update Constants.APP_VERSION when changing this version
val projectVersion = "1.4.0-rc01" val projectVersion = "1.4.1"
val projectName = "Gitnuro" val projectName = "Gitnuro"
// Required for JPackage, as it doesn't accept additional suffixes after the version. // Required for JPackage, as it doesn't accept additional suffixes after the version.
val projectVersionSimplified = "1.4.0" val projectVersionSimplified = "1.4.1"
val rustGeneratedSource = "${layout.buildDirectory.get()}/generated/source/uniffi/main/com/jetpackduba/gitnuro/java" val rustGeneratedSource = "${layout.buildDirectory.get()}/generated/source/uniffi/main/com/jetpackduba/gitnuro/java"

View File

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

View File

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

View File

@ -22,8 +22,8 @@ object AppConstants {
const val APP_NAME = "Gitnuro" const val APP_NAME = "Gitnuro"
const val APP_DESCRIPTION = 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." "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.0-rc01" const val APP_VERSION = "1.4.1"
const val APP_VERSION_CODE = 13 const val APP_VERSION_CODE = 15
const val VERSION_CHECK_URL = "https://raw.githubusercontent.com/JetpackDuba/Gitnuro/main/latest.json" const val VERSION_CHECK_URL = "https://raw.githubusercontent.com/JetpackDuba/Gitnuro/main/latest.json"
} }

View File

@ -2,6 +2,7 @@ package com.jetpackduba.gitnuro.credentials
import com.jetpackduba.gitnuro.exceptions.NotSupportedHelper import com.jetpackduba.gitnuro.exceptions.NotSupportedHelper
import com.jetpackduba.gitnuro.git.remote_operations.CredentialsCache import com.jetpackduba.gitnuro.git.remote_operations.CredentialsCache
import com.jetpackduba.gitnuro.logging.printError
import com.jetpackduba.gitnuro.logging.printLog import com.jetpackduba.gitnuro.logging.printLog
import com.jetpackduba.gitnuro.managers.IShellManager import com.jetpackduba.gitnuro.managers.IShellManager
import com.jetpackduba.gitnuro.repositories.AppSettingsRepository import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
@ -259,7 +260,8 @@ class HttpCredentialsProvider @AssistedInject constructor(
val credentialHelperPath = uriSpecificCredentialHelper ?: genericCredentialHelper ?: return null val credentialHelperPath = uriSpecificCredentialHelper ?: genericCredentialHelper ?: return null
if (credentialHelperPath == "cache" || credentialHelperPath == "store") { if (credentialHelperPath == "cache" || credentialHelperPath == "store") {
throw NotSupportedHelper("Invalid credentials helper: \"$credentialHelperPath\" is not yet supported") printError(TAG, "Invalid credentials helper: \"$credentialHelperPath\" is not yet supported")
return null
} }
// TODO Try to use "git-credential-manager-core" when "manager-core" is detected. Works for linux but requires testing for mac/windows // 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,11 +1,6 @@
package com.jetpackduba.gitnuro.exceptions package com.jetpackduba.gitnuro.exceptions
class WatcherInitException( fun codeToMessage(code: Int): String {
code: Int,
message: String = codeToMessage(code),
) : GitnuroException(message)
private fun codeToMessage(code: Int): String {
return when (code) { return when (code) {
1 /*is WatcherInitException.Generic*/, 2 /*is WatcherInitException.Io*/ -> "Could not watch directory. Check if it exists and you have read permissions." 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" 3 /*is WatcherInitException.PathNotFound*/ -> "Path not found, check if your repository still exists"

View File

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

View File

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

View File

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

View File

@ -0,0 +1,34 @@
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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.CredentialsProvider
import javax.inject.Inject import javax.inject.Inject
class PullBranchUseCase @Inject constructor( class PullBranchUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase, private val handleTransportUseCase: HandleTransportUseCase,
private val appSettingsRepository: AppSettingsRepository, private val appSettingsRepository: AppSettingsRepository,
private val hasPullResultConflictsUseCase: HasPullResultConflictsUseCase,
) { ) {
suspend operator fun invoke(git: Git, pullType: PullType) = withContext(Dispatchers.IO) { suspend operator fun invoke(git: Git, pullType: PullType): PullHasConflicts = withContext(Dispatchers.IO) {
val pullWithRebase = when (pullType) { val pullWithRebase = when (pullType) {
PullType.REBASE -> true PullType.REBASE -> true
PullType.MERGE -> false PullType.MERGE -> false
@ -27,19 +27,7 @@ class PullBranchUseCase @Inject constructor(
.setCredentialsProvider(CredentialsProvider.getDefault()) .setCredentialsProvider(CredentialsProvider.getDefault())
.call() .call()
if (!pullResult.isSuccessful) { return@handleTransportUseCase hasPullResultConflictsUseCase(pullWithRebase, pullResult)
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,42 +2,34 @@ package com.jetpackduba.gitnuro.git.remote_operations
import com.jetpackduba.gitnuro.extensions.remoteName import com.jetpackduba.gitnuro.extensions.remoteName
import com.jetpackduba.gitnuro.extensions.simpleName import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.CredentialsProvider
import javax.inject.Inject import javax.inject.Inject
class PullFromSpecificBranchUseCase @Inject constructor( class PullFromSpecificBranchUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase, private val handleTransportUseCase: HandleTransportUseCase,
private val hasPullResultConflictsUseCase: HasPullResultConflictsUseCase,
private val appSettingsRepository: AppSettingsRepository,
) { ) {
suspend operator fun invoke(git: Git, rebase: Boolean, remoteBranch: Ref) = withContext(Dispatchers.IO) { suspend operator fun invoke(git: Git, remoteBranch: Ref): PullHasConflicts =
handleTransportUseCase(git) { withContext(Dispatchers.IO) {
val pullResult = git val pullWithRebase = appSettingsRepository.pullRebase
.pull()
.setTransportConfigCallback { handleTransport(it) }
.setRemote(remoteBranch.remoteName)
.setRemoteBranchName(remoteBranch.simpleName)
.setRebase(rebase)
.setCredentialsProvider(CredentialsProvider.getDefault())
.call()
if (!pullResult.isSuccessful) { handleTransportUseCase(git) {
var message = val pullResult = git
"Pull failed" // TODO Remove messages from here and pass the result to a custom exception type .pull()
.setTransportConfigCallback { handleTransport(it) }
.setRemote(remoteBranch.remoteName)
.setRemoteBranchName(remoteBranch.simpleName)
.setRebase(pullWithRebase)
.setCredentialsProvider(CredentialsProvider.getDefault())
.call()
if (rebase) { return@handleTransportUseCase hasPullResultConflictsUseCase(pullWithRebase, pullResult)
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,11 +142,9 @@ private fun baseKeybindings() = mapOf(
), ),
KeybindingOption.CHANGE_CURRENT_TAB_LEFT to listOf( KeybindingOption.CHANGE_CURRENT_TAB_LEFT to listOf(
Keybinding(key = Key.DirectionLeft, alt = true), Keybinding(key = Key.DirectionLeft, alt = true),
Keybinding(key = Key.Tab, control = true, shift = true),
), ),
KeybindingOption.CHANGE_CURRENT_TAB_RIGHT to listOf( KeybindingOption.CHANGE_CURRENT_TAB_RIGHT to listOf(
Keybinding(key = Key.DirectionRight, alt = true), Keybinding(key = Key.DirectionRight, alt = true),
Keybinding(key = Key.Tab, control = true),
), ),
) )
@ -157,26 +155,10 @@ private fun macKeybindings(): Map<KeybindingOption, List<Keybinding>> {
val macBindings = baseKeybindings().toMutableMap() val macBindings = baseKeybindings().toMutableMap()
macBindings.apply { macBindings.apply {
val keysToReplaceControlWithCommand = listOf( this[KeybindingOption.REFRESH] = listOf(
KeybindingOption.REFRESH, Keybinding(key = Key.F5),
KeybindingOption.PULL, Keybinding(meta = true, key = Key.R),
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 return macBindings
@ -204,6 +186,3 @@ fun KeyEvent.matchesBinding(keybindingOption: KeybindingOption): Boolean {
keybinding.key == this.key keybinding.key == this.key
} && this.type == KeyEventType.KeyDown } && this.type == KeyEventType.KeyDown
} }
val KeybindingOption.keyBinding
get() = keybindings[this]?.firstOrNull()

View File

@ -2,7 +2,6 @@
package com.jetpackduba.gitnuro.ui package com.jetpackduba.gitnuro.ui
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -21,12 +20,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter 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.LayoutCoordinates
import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
@ -37,11 +34,6 @@ import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.extensions.handOnHover import com.jetpackduba.gitnuro.extensions.handOnHover
import com.jetpackduba.gitnuro.extensions.ignoreKeyEvents import com.jetpackduba.gitnuro.extensions.ignoreKeyEvents
import com.jetpackduba.gitnuro.git.remote_operations.PullType 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.PrimaryButton
import com.jetpackduba.gitnuro.ui.components.tooltip.InstantTooltip import com.jetpackduba.gitnuro.ui.components.tooltip.InstantTooltip
import com.jetpackduba.gitnuro.ui.context_menu.* import com.jetpackduba.gitnuro.ui.context_menu.*
@ -70,17 +62,19 @@ fun Menu(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
MenuButton( InstantTooltip(
modifier = Modifier text = "Open a different repository",
.padding(start = 16.dp) enabled = !showOpenPopup,
.onGloballyPositioned { setPosition(it) }, ) {
title = "Open", MenuButton(
icon = painterResource(AppIcons.OPEN), modifier = Modifier
keybinding = KeybindingOption.OPEN_REPOSITORY.keyBinding, .padding(start = 16.dp)
tooltip = "Open a different repository", .onGloballyPositioned { setPosition(it) },
tooltipEnabled = !showOpenPopup, title = "Open",
onClick = { onShowOpenPopupChange(true) }, icon = painterResource(AppIcons.OPEN),
) onClick = { onShowOpenPopupChange(true) },
)
}
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
@ -132,15 +126,16 @@ fun Menu(
Spacer(modifier = Modifier.width(32.dp)) Spacer(modifier = Modifier.width(32.dp))
MenuButton( InstantTooltip(
title = "Branch", text = "Create a new branch",
icon = painterResource(AppIcons.BRANCH), ) {
onClick = { MenuButton(
title = "Branch",
icon = painterResource(AppIcons.BRANCH),
) {
onCreateBranch() onCreateBranch()
}, }
tooltip = "Create a new branch", }
keybinding = KeybindingOption.BRANCH_CREATE.keyBinding,
)
Spacer(modifier = Modifier.width(32.dp)) Spacer(modifier = Modifier.width(32.dp))
@ -156,42 +151,43 @@ fun Menu(
) )
) )
MenuButton( InstantTooltip(
title = "Pop", text = "Pop the last stash"
icon = painterResource(AppIcons.APPLY_STASH), ) {
keybinding = KeybindingOption.STASH_POP.keyBinding, MenuButton(
tooltip = "Pop the last stash", title = "Pop",
) { menuViewModel.popStash() } icon = painterResource(AppIcons.APPLY_STASH),
) { menuViewModel.popStash() }
}
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
MenuButton( InstantTooltip(
modifier = Modifier.padding(end = 4.dp), text = "Open a terminal in the repository's path"
title = "Terminal", ) {
icon = painterResource(AppIcons.TERMINAL), MenuButton(
onClick = { menuViewModel.openTerminal() }, modifier = Modifier.padding(end = 4.dp),
tooltip = "Open a terminal in the repository's path", title = "Terminal",
keybinding = null, icon = painterResource(AppIcons.TERMINAL),
) onClick = { menuViewModel.openTerminal() },
)
}
MenuButton( MenuButton(
modifier = Modifier.padding(end = 4.dp), modifier = Modifier.padding(end = 4.dp),
title = "Actions", title = "Actions",
icon = painterResource(AppIcons.BOLT), icon = painterResource(AppIcons.BOLT),
onClick = onQuickActions, onClick = onQuickActions,
tooltip = "Additional actions",
keybinding = KeybindingOption.STASH_POP.keyBinding,
) )
Box( InstantTooltip(
text = "Gitnuro's settings",
modifier = Modifier.padding(end = 16.dp) modifier = Modifier.padding(end = 16.dp)
) { ) {
MenuButton( MenuButton(
title = "Settings", title = "Settings",
icon = painterResource(AppIcons.SETTINGS), icon = painterResource(AppIcons.SETTINGS),
onClick = onShowSettingsDialog, onClick = onShowSettingsDialog,
tooltip = "Gitnuro's settings",
keybinding = KeybindingOption.STASH_POP.keyBinding,
) )
} }
} }
@ -261,135 +257,33 @@ fun MenuButton(
enabled: Boolean = true, enabled: Boolean = true,
title: String, title: String,
icon: Painter, icon: Painter,
keybinding: Keybinding?, onClick: () -> Unit
tooltip: String,
tooltipEnabled: Boolean = true,
onClick: () -> Unit,
) { ) {
InstantTooltip( Column(
text = tooltip, modifier = modifier
enabled = tooltipEnabled, .ignoreKeyEvents()
trailingContent = if (keybinding != null) {
{ KeybindingHint(keybinding) }
} else {
null
}
) {
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)) .clip(RoundedCornerShape(4.dp))
.border(2.dp, MaterialTheme.colors.primary, RoundedCornerShape(4.dp)) .background(MaterialTheme.colors.surface)
.background(MaterialTheme.colors.primary.copy(alpha = 0.05f)) .handMouseClickable { if (enabled) onClick() }
.padding(horizontal = 4.dp, vertical = 4.dp) .size(56.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) verticalArrangement = Arrangement.Center,
} ) {
Icon(
fun getParts(keybinding: Keybinding): List<String> { painter = icon,
val parts = mutableListOf<String>() contentDescription = title,
modifier = Modifier
if (keybinding.control) { .size(24.dp),
parts.add("Ctrl") tint = MaterialTheme.colors.onBackground,
)
Text(
text = title,
style = MaterialTheme.typography.caption,
maxLines = 1,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onBackground,
)
} }
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 @Composable

View File

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

View File

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

View File

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

View File

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

View File

@ -1,25 +1,10 @@
package com.jetpackduba.gitnuro.viewmodels 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.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.repositories.AppSettingsRepository
import com.jetpackduba.gitnuro.terminal.OpenRepositoryInTerminalUseCase
import javax.inject.Inject import javax.inject.Inject
class MenuViewModel @Inject constructor( class MenuViewModel @Inject constructor(
private val tabState: TabState,
private val globalMenuActionsViewModel: GlobalMenuActionsViewModel, private val globalMenuActionsViewModel: GlobalMenuActionsViewModel,
settings: AppSettingsRepository, settings: AppSettingsRepository,
appStateManager: AppStateManager, appStateManager: AppStateManager,

View File

@ -2,7 +2,7 @@ package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.SharedRepositoryStateManager import com.jetpackduba.gitnuro.SharedRepositoryStateManager
import com.jetpackduba.gitnuro.TaskType import com.jetpackduba.gitnuro.TaskType
import com.jetpackduba.gitnuro.exceptions.WatcherInitException import com.jetpackduba.gitnuro.exceptions.codeToMessage
import com.jetpackduba.gitnuro.git.* import com.jetpackduba.gitnuro.git.*
import com.jetpackduba.gitnuro.git.branches.CreateBranchUseCase import com.jetpackduba.gitnuro.git.branches.CreateBranchUseCase
import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState
@ -110,6 +110,8 @@ class RepositoryOpenViewModel @Inject constructor(
var authorViewModel: AuthorViewModel? = null var authorViewModel: AuthorViewModel? = null
private set private set
private var hasGitDirChanged = false
init { init {
tabScope.run { tabScope.run {
launch { launch {
@ -119,7 +121,7 @@ class RepositoryOpenViewModel @Inject constructor(
} }
launch { launch {
watchRepositoryChanges(tabState.git) watchRepositoryChanges()
} }
} }
} }
@ -158,55 +160,53 @@ class RepositoryOpenViewModel @Inject constructor(
* the app by constantly running "git status" or even full refreshes. * the app by constantly running "git status" or even full refreshes.
* *
*/ */
private suspend fun watchRepositoryChanges(git: Git) = tabScope.launch(Dispatchers.IO) { private suspend fun watchRepositoryChanges() = tabScope.launch(Dispatchers.IO) {
var hasGitDirChanged = false
launch { launch {
fileChangesWatcher.changesNotifier.collect { latestUpdateChangedGitDir -> fileChangesWatcher.changesNotifier.collect { watcherEvent ->
val isOperationRunning = tabState.operationRunning 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,
),
)
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")
} }
} }
} }
}
try { private suspend fun CoroutineScope.repositoryChanged(hasGitDirChanged: Boolean) {
fileChangesWatcher.watchDirectoryPath( val isOperationRunning = tabState.operationRunning
repository = git.repository,
) if (!isOperationRunning) { // Only update if there isn't any process running
} catch (ex: WatcherInitException) { printDebug(TAG, "Detected changes in the repository's directory")
val message = ex.message
if (message != null) { val currentTimeMillis = System.currentTimeMillis()
errorsManager.addError(
newErrorNow( if (
exception = ex, hasGitDirChanged &&
taskType = TaskType.CHANGES_DETECTION, currentTimeMillis - tabState.lastOperation < MIN_TIME_AFTER_GIT_OPERATION
), ) {
) printDebug(TAG, "Git operation was executed recently, ignoring file system change")
return
} }
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,6 +9,7 @@ import com.jetpackduba.gitnuro.git.remote_operations.DeleteRemoteBranchUseCase
import com.jetpackduba.gitnuro.git.remote_operations.PullFromSpecificBranchUseCase import com.jetpackduba.gitnuro.git.remote_operations.PullFromSpecificBranchUseCase
import com.jetpackduba.gitnuro.git.remote_operations.PushToSpecificBranchUseCase import com.jetpackduba.gitnuro.git.remote_operations.PushToSpecificBranchUseCase
import com.jetpackduba.gitnuro.models.positiveNotification import com.jetpackduba.gitnuro.models.positiveNotification
import com.jetpackduba.gitnuro.models.warningNotification
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
import javax.inject.Inject import javax.inject.Inject
@ -69,12 +70,10 @@ class SharedRemotesViewModel @Inject constructor(
subtitle = "Pulling changes from ${branch.simpleName} to the current branch", subtitle = "Pulling changes from ${branch.simpleName} to the current branch",
taskType = TaskType.PULL_FROM_BRANCH, taskType = TaskType.PULL_FROM_BRANCH,
) { git -> ) { git ->
pullFromSpecificBranchUseCase( if (pullFromSpecificBranchUseCase(git = git, remoteBranch = branch)) {
git = git, warningNotification("Pull produced conflicts, fix them to continue")
rebase = false, } else {
remoteBranch = branch, positiveNotification("Pulled from \"${branch.simpleName}\"")
) }
positiveNotification("Pulled from \"${branch.simpleName}\"")
} }
} }

View File

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

View File

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