Compare commits
12 Commits
keybinding
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
edcefd7a38 | ||
|
01b0827057 | ||
|
db1467c354 | ||
|
07e98dae10 | ||
|
644f33ff2c | ||
|
3128341b93 | ||
|
1ef596ad3c | ||
|
4cc87a7289 | ||
|
fcecd0380a | ||
|
fc2098781b | ||
|
ce694ed44b | ||
|
e5899d02d6 |
@ -8,6 +8,8 @@
|
||||
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
|
||||
|
@ -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-beta02"
|
||||
id("org.jetbrains.compose") version "1.7.0"
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.0.20"
|
||||
}
|
||||
|
||||
// Remember to update Constants.APP_VERSION when changing this version
|
||||
val projectVersion = "1.4.0-rc01"
|
||||
val projectVersion = "1.4.1"
|
||||
|
||||
val projectName = "Gitnuro"
|
||||
|
||||
// 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"
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"appVersion": "1.3.1",
|
||||
"appCode": 11,
|
||||
"downloadUrl": "https://github.com/JetpackDuba/Gitnuro/releases/tag/v1.3.1"
|
||||
"appVersion": "1.4.0",
|
||||
"appCode": 14,
|
||||
"downloadUrl": "https://github.com/JetpackDuba/Gitnuro/releases/tag/v1.4.0"
|
||||
}
|
||||
|
@ -277,7 +277,6 @@ class App {
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.background)
|
||||
.onPreviewKeyEvent {
|
||||
println(it.toString())
|
||||
when {
|
||||
it.matchesBinding(KeybindingOption.OPEN_NEW_TAB) -> {
|
||||
tabsManager.addNewEmptyTab()
|
||||
|
@ -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.0-rc01"
|
||||
const val APP_VERSION_CODE = 13
|
||||
const val APP_VERSION = "1.4.1"
|
||||
const val APP_VERSION_CODE = 15
|
||||
const val VERSION_CHECK_URL = "https://raw.githubusercontent.com/JetpackDuba/Gitnuro/main/latest.json"
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ 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
|
||||
@ -259,7 +260,8 @@ class HttpCredentialsProvider @AssistedInject constructor(
|
||||
val credentialHelperPath = uriSpecificCredentialHelper ?: genericCredentialHelper ?: return null
|
||||
|
||||
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
|
||||
|
@ -1,11 +1,6 @@
|
||||
package com.jetpackduba.gitnuro.exceptions
|
||||
|
||||
class WatcherInitException(
|
||||
code: Int,
|
||||
message: String = codeToMessage(code),
|
||||
) : GitnuroException(message)
|
||||
|
||||
private fun codeToMessage(code: Int): String {
|
||||
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"
|
||||
|
@ -3,7 +3,6 @@ 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
|
||||
@ -25,8 +24,8 @@ class FileChangesWatcher @Inject constructor(
|
||||
private val getIgnoreRulesUseCase: GetIgnoreRulesUseCase,
|
||||
private val tabScope: CoroutineScope,
|
||||
) : AutoCloseable {
|
||||
private val _changesNotifier = MutableSharedFlow<Boolean>()
|
||||
val changesNotifier: SharedFlow<Boolean> = _changesNotifier
|
||||
private val _changesNotifier = MutableSharedFlow<WatcherEvent>()
|
||||
val changesNotifier: SharedFlow<WatcherEvent> = _changesNotifier
|
||||
private val fileWatcher = FileWatcher.new()
|
||||
private var shouldKeepLooping = true
|
||||
|
||||
@ -71,14 +70,16 @@ class FileChangesWatcher @Inject constructor(
|
||||
|
||||
if (!areAllPathsIgnored) {
|
||||
println("Emitting changes $hasGitIgnoreChanged")
|
||||
_changesNotifier.emit(hasGitDirChanged)
|
||||
_changesNotifier.emit(WatcherEvent.RepositoryChanged(hasGitDirChanged))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(code: Int) {
|
||||
throw WatcherInitException(code)
|
||||
tabScope.launch {
|
||||
_changesNotifier.emit(WatcherEvent.WatchInitError(code))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,4 +91,9 @@ class FileChangesWatcher @Inject constructor(
|
||||
fileWatcher.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed interface WatcherEvent {
|
||||
data class RepositoryChanged(val hasGitDirChanged: Boolean) : WatcherEvent
|
||||
data class WatchInitError(val code: Int) : WatcherEvent
|
||||
}
|
@ -15,9 +15,12 @@ class SaveAuthorUseCase @Inject constructor() {
|
||||
repoConfig.load()
|
||||
|
||||
if (globalConfig is FileBasedConfig) {
|
||||
globalConfig.setStringProperty("user", null, "name", newAuthorInfo.globalName)
|
||||
globalConfig.setStringProperty("user", null, "email", newAuthorInfo.globalEmail)
|
||||
globalConfig.save()
|
||||
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()
|
||||
}
|
||||
|
||||
config.setStringProperty("user", null, "name", newAuthorInfo.name)
|
||||
|
@ -37,8 +37,7 @@ 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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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) = withContext(Dispatchers.IO) {
|
||||
suspend operator fun invoke(git: Git, pullType: PullType): PullHasConflicts = withContext(Dispatchers.IO) {
|
||||
val pullWithRebase = when (pullType) {
|
||||
PullType.REBASE -> true
|
||||
PullType.MERGE -> false
|
||||
@ -27,19 +27,7 @@ class PullBranchUseCase @Inject constructor(
|
||||
.setCredentialsProvider(CredentialsProvider.getDefault())
|
||||
.call()
|
||||
|
||||
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)
|
||||
}
|
||||
return@handleTransportUseCase hasPullResultConflictsUseCase(pullWithRebase, pullResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,42 +2,34 @@ 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, 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()
|
||||
suspend operator fun invoke(git: Git, remoteBranch: Ref): PullHasConflicts =
|
||||
withContext(Dispatchers.IO) {
|
||||
val pullWithRebase = appSettingsRepository.pullRebase
|
||||
|
||||
if (!pullResult.isSuccessful) {
|
||||
var message =
|
||||
"Pull failed" // TODO Remove messages from here and pass the result to a custom exception type
|
||||
handleTransportUseCase(git) {
|
||||
val pullResult = git
|
||||
.pull()
|
||||
.setTransportConfigCallback { handleTransport(it) }
|
||||
.setRemote(remoteBranch.remoteName)
|
||||
.setRemoteBranchName(remoteBranch.simpleName)
|
||||
.setRebase(pullWithRebase)
|
||||
.setCredentialsProvider(CredentialsProvider.getDefault())
|
||||
.call()
|
||||
|
||||
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)
|
||||
return@handleTransportUseCase hasPullResultConflictsUseCase(pullWithRebase, pullResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -142,11 +142,9 @@ 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),
|
||||
),
|
||||
)
|
||||
|
||||
@ -157,26 +155,10 @@ private fun macKeybindings(): Map<KeybindingOption, List<Keybinding>> {
|
||||
val macBindings = baseKeybindings().toMutableMap()
|
||||
|
||||
macBindings.apply {
|
||||
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,
|
||||
this[KeybindingOption.REFRESH] = listOf(
|
||||
Keybinding(key = Key.F5),
|
||||
Keybinding(meta = true, key = Key.R),
|
||||
)
|
||||
|
||||
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
|
||||
@ -203,7 +185,4 @@ 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()
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
|
||||
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
|
||||
@ -21,12 +20,10 @@ 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
|
||||
@ -37,11 +34,6 @@ 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.*
|
||||
@ -70,17 +62,19 @@ fun Menu(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
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) },
|
||||
)
|
||||
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) },
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
@ -132,15 +126,16 @@ fun Menu(
|
||||
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
|
||||
MenuButton(
|
||||
title = "Branch",
|
||||
icon = painterResource(AppIcons.BRANCH),
|
||||
onClick = {
|
||||
InstantTooltip(
|
||||
text = "Create a new branch",
|
||||
) {
|
||||
MenuButton(
|
||||
title = "Branch",
|
||||
icon = painterResource(AppIcons.BRANCH),
|
||||
) {
|
||||
onCreateBranch()
|
||||
},
|
||||
tooltip = "Create a new branch",
|
||||
keybinding = KeybindingOption.BRANCH_CREATE.keyBinding,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
@ -156,42 +151,43 @@ fun Menu(
|
||||
)
|
||||
)
|
||||
|
||||
MenuButton(
|
||||
title = "Pop",
|
||||
icon = painterResource(AppIcons.APPLY_STASH),
|
||||
keybinding = KeybindingOption.STASH_POP.keyBinding,
|
||||
tooltip = "Pop the last stash",
|
||||
) { menuViewModel.popStash() }
|
||||
InstantTooltip(
|
||||
text = "Pop the last stash"
|
||||
) {
|
||||
MenuButton(
|
||||
title = "Pop",
|
||||
icon = painterResource(AppIcons.APPLY_STASH),
|
||||
) { menuViewModel.popStash() }
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
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,
|
||||
)
|
||||
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 = "Actions",
|
||||
icon = painterResource(AppIcons.BOLT),
|
||||
onClick = onQuickActions,
|
||||
tooltip = "Additional actions",
|
||||
keybinding = KeybindingOption.STASH_POP.keyBinding,
|
||||
)
|
||||
|
||||
Box(
|
||||
InstantTooltip(
|
||||
text = "Gitnuro's settings",
|
||||
modifier = Modifier.padding(end = 16.dp)
|
||||
) {
|
||||
MenuButton(
|
||||
title = "Settings",
|
||||
icon = painterResource(AppIcons.SETTINGS),
|
||||
onClick = onShowSettingsDialog,
|
||||
tooltip = "Gitnuro's settings",
|
||||
keybinding = KeybindingOption.STASH_POP.keyBinding,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -261,135 +257,33 @@ fun MenuButton(
|
||||
enabled: Boolean = true,
|
||||
title: String,
|
||||
icon: Painter,
|
||||
keybinding: Keybinding?,
|
||||
tooltip: String,
|
||||
tooltipEnabled: Boolean = true,
|
||||
onClick: () -> Unit,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
InstantTooltip(
|
||||
text = tooltip,
|
||||
enabled = tooltipEnabled,
|
||||
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
|
||||
Column(
|
||||
modifier = modifier
|
||||
.ignoreKeyEvents()
|
||||
.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")
|
||||
.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,
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -89,7 +89,6 @@ fun UncommittedChanges(
|
||||
val doCommit = {
|
||||
statusViewModel.commit(commitMessage)
|
||||
onStagedDiffEntrySelected(null)
|
||||
setCommitMessage("")
|
||||
}
|
||||
|
||||
val canCommit = commitMessage.isNotEmpty() && stageStateUi.hasStagedFiles
|
||||
|
@ -28,7 +28,6 @@ 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,
|
||||
@ -72,14 +71,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)
|
||||
}
|
||||
|
||||
Row(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.shadow(8.dp)
|
||||
@ -95,21 +94,15 @@ fun InstantTooltip(
|
||||
)
|
||||
} else
|
||||
this
|
||||
}
|
||||
.padding(8.dp),
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 12.sp,
|
||||
maxLines = 1,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
|
||||
if (trailingContent != null) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
trailingContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,9 @@ fun ErrorDialog(
|
||||
val clipboard = LocalClipboardManager.current
|
||||
var showStackTrace by remember { mutableStateOf(false) }
|
||||
|
||||
MaterialDialog {
|
||||
MaterialDialog (
|
||||
onCloseRequested = onAccept,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(580.dp)
|
||||
|
@ -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.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.repositories.AppSettingsRepository
|
||||
import com.jetpackduba.gitnuro.models.warningNotification
|
||||
import com.jetpackduba.gitnuro.terminal.OpenRepositoryInTerminalUseCase
|
||||
import kotlinx.coroutines.Job
|
||||
import javax.inject.Inject
|
||||
@ -26,7 +25,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,
|
||||
@ -34,8 +33,6 @@ 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,
|
||||
@ -44,9 +41,11 @@ class GlobalMenuActionsViewModel @Inject constructor(
|
||||
refreshEvenIfCrashes = true,
|
||||
taskType = TaskType.PULL,
|
||||
) { git ->
|
||||
pullBranchUseCase(git, pullType)
|
||||
|
||||
positiveNotification("Pull completed")
|
||||
if (pullBranchUseCase(git, pullType)) {
|
||||
warningNotification("Pull produced conflicts, fix them to continue")
|
||||
} else {
|
||||
positiveNotification("Pull completed")
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchAll() = tabState.safeProcessing(
|
||||
|
@ -1,25 +1,10 @@
|
||||
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,
|
||||
|
@ -2,7 +2,7 @@ package com.jetpackduba.gitnuro.viewmodels
|
||||
|
||||
import com.jetpackduba.gitnuro.SharedRepositoryStateManager
|
||||
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.branches.CreateBranchUseCase
|
||||
import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState
|
||||
@ -110,6 +110,8 @@ class RepositoryOpenViewModel @Inject constructor(
|
||||
var authorViewModel: AuthorViewModel? = null
|
||||
private set
|
||||
|
||||
private var hasGitDirChanged = false
|
||||
|
||||
init {
|
||||
tabScope.run {
|
||||
launch {
|
||||
@ -119,7 +121,7 @@ class RepositoryOpenViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
launch {
|
||||
watchRepositoryChanges(tabState.git)
|
||||
watchRepositoryChanges()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -158,55 +160,53 @@ class RepositoryOpenViewModel @Inject constructor(
|
||||
* the app by constantly running "git status" or even full refreshes.
|
||||
*
|
||||
*/
|
||||
private suspend fun watchRepositoryChanges(git: Git) = tabScope.launch(Dispatchers.IO) {
|
||||
var hasGitDirChanged = false
|
||||
|
||||
private suspend fun watchRepositoryChanges() = tabScope.launch(Dispatchers.IO) {
|
||||
launch {
|
||||
fileChangesWatcher.changesNotifier.collect { latestUpdateChangedGitDir ->
|
||||
val isOperationRunning = tabState.operationRunning
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
||||
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 {
|
||||
fileChangesWatcher.watchDirectoryPath(
|
||||
repository = git.repository,
|
||||
)
|
||||
} catch (ex: WatcherInitException) {
|
||||
val message = ex.message
|
||||
if (message != null) {
|
||||
errorsManager.addError(
|
||||
newErrorNow(
|
||||
exception = ex,
|
||||
taskType = TaskType.CHANGES_DETECTION,
|
||||
),
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
if (hasGitDirChanged) {
|
||||
this@RepositoryOpenViewModel.hasGitDirChanged = true
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
updateApp(hasGitDirChanged)
|
||||
}
|
||||
|
||||
this@RepositoryOpenViewModel.hasGitDirChanged = false
|
||||
} else {
|
||||
printDebug(TAG, "Ignored file events during operation")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.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
|
||||
@ -69,12 +70,10 @@ class SharedRemotesViewModel @Inject constructor(
|
||||
subtitle = "Pulling changes from ${branch.simpleName} to the current branch",
|
||||
taskType = TaskType.PULL_FROM_BRANCH,
|
||||
) { git ->
|
||||
pullFromSpecificBranchUseCase(
|
||||
git = git,
|
||||
rebase = false,
|
||||
remoteBranch = branch,
|
||||
)
|
||||
|
||||
positiveNotification("Pulled from \"${branch.simpleName}\"")
|
||||
if (pullFromSpecificBranchUseCase(git = git, remoteBranch = branch)) {
|
||||
warningNotification("Pull produced conflicts, fix them to continue")
|
||||
} else {
|
||||
positiveNotification("Pulled from \"${branch.simpleName}\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -395,7 +395,9 @@ 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")
|
||||
|
@ -1,45 +1,37 @@
|
||||
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.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.FileChangesWatcher
|
||||
import com.jetpackduba.gitnuro.git.ProcessingState
|
||||
import com.jetpackduba.gitnuro.git.RefreshType
|
||||
import com.jetpackduba.gitnuro.git.TabState
|
||||
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.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
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 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
|
||||
|
Loading…
Reference in New Issue
Block a user