Compare commits

..

23 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
Abdelilah El Aissaoui
169ed5af3f
Added warning when rebase has conflicts or has stopped 2024-09-17 23:40:24 +02:00
Abdelilah El Aissaoui
712e513c2e
Changed gitnuro version to 1.4.0-rc01 2024-09-14 20:01:00 +02:00
Abdelilah El Aissaoui
02a1b47eeb
Disabled MacOS build until fixed (+ out of CI minutes...) 2024-09-14 19:59:43 +02:00
Abdelilah El Aissaoui
28a465441e
Changed rust build to be release by default 2024-09-14 19:57:39 +02:00
Abdelilah El Aissaoui
aab0e01a30
Open repository keybinding can also be used in welcome page to open file explorer 2024-09-14 15:50:08 +02:00
Abdelilah El Aissaoui
fb542c9677
Welcome page focus is set to the whole page if there aren't recent repositories 2024-09-14 15:49:44 +02:00
Abdelilah El Aissaoui
e9c3e25d79
Fixed settings label 2024-09-14 15:26:04 +02:00
Abdelilah El Aissaoui
4423e47019
Added tabs management keybindings 2024-09-14 15:25:04 +02:00
Abdelilah El Aissaoui
1b4b75d75b
Refactored RepositoryOpenViewModel to fix file system watch not working 2024-09-14 15:04:24 +02:00
Abdelilah El Aissaoui
8b17d68db4
Refactored getting author info 2024-09-14 15:03:20 +02:00
Abdelilah El Aissaoui
87399eccf4
Added set the tracking branch when pushing a new branch 2024-09-14 01:19:13 +02:00
38 changed files with 418 additions and 301 deletions

View File

@ -124,42 +124,42 @@ jobs:
Output/Gitnuro*.zip
token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
build_macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: nightly
- run: cargo install cargo-kotars --git https://github.com/JetpackDuba/kotars
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'corretto'
architecture: x64
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: createDistributable
- name: Create output directory
run: mkdir Output
- name: MacOS DMG
working-directory: build/compose/binaries/main/app/
run: zip -r ../../../../../Output/Gitnuro_macos_${{github.ref_name}}.zip .
- name: Generate SHA256 Checksum
working-directory: ./Output/
run: find . -type f -exec bash -c "shasum -a 256 {} > {}.sum " \;
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
body: "Beta release"
prerelease: true
draft: true
repository: JetpackDuba/Gitnuro
with:
files: |
Output/Gitnuro*.zip
Output/Gitnuro*.sum
token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
# build_macos:
# runs-on: macos-latest
# steps:
# - uses: actions/checkout@v3
# - uses: dtolnay/rust-toolchain@stable
# with:
# toolchain: nightly
# - run: cargo install cargo-kotars --git https://github.com/JetpackDuba/kotars
# - name: Set up JDK 17
# uses: actions/setup-java@v3
# with:
# java-version: '17'
# distribution: 'corretto'
# architecture: x64
# - name: Build with Gradle
# uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
# with:
# arguments: createDistributable
# - name: Create output directory
# run: mkdir Output
# - name: MacOS DMG
# working-directory: build/compose/binaries/main/app/
# run: zip -r ../../../../../Output/Gitnuro_macos_${{github.ref_name}}.zip .
# - name: Generate SHA256 Checksum
# working-directory: ./Output/
# run: find . -type f -exec bash -c "shasum -a 256 {} > {}.sum " \;
# - name: Release
# uses: softprops/action-gh-release@v2
# if: startsWith(github.ref, 'refs/tags/')
# with:
# body: "Beta release"
# prerelease: true
# draft: true
# repository: JetpackDuba/Gitnuro
# with:
# files: |
# Output/Gitnuro*.zip
# Output/Gitnuro*.sum
# token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}

View File

@ -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

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-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-beta01"
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"
@ -47,7 +47,7 @@ repositories {
}
dependencies {
val jgit = "7.0.0.202409031743-r"
val jgit = "6.9.0.202403050737-r"
if (currentOs() == OS.LINUX && isLinuxAarch64) {
implementation(compose.desktop.linux_arm64)

View File

@ -1,4 +1,4 @@
kotlin.code.style=official
isLinuxAarch64=false
useCross=false
isRustRelease=false
isRustRelease=true

View File

@ -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"
}

View File

@ -12,6 +12,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Density
@ -25,6 +26,8 @@ import com.jetpackduba.gitnuro.di.DaggerAppComponent
import com.jetpackduba.gitnuro.extensions.preferenceValue
import com.jetpackduba.gitnuro.extensions.toWindowPlacement
import com.jetpackduba.gitnuro.git.AppGpgSigner
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.logging.printError
import com.jetpackduba.gitnuro.managers.AppStateManager
import com.jetpackduba.gitnuro.managers.TempFilesManager
@ -43,8 +46,7 @@ import com.jetpackduba.gitnuro.ui.components.TabInformation
import com.jetpackduba.gitnuro.ui.context_menu.AppPopupMenu
import com.jetpackduba.gitnuro.ui.dialogs.settings.ProxyType
import kotlinx.coroutines.launch
import org.eclipse.jgit.lib.Signer
import org.eclipse.jgit.lib.SignerFactory
import org.eclipse.jgit.lib.GpgSigner
import java.io.File
import java.io.FileOutputStream
import java.net.Authenticator
@ -65,6 +67,9 @@ class App {
@Inject
lateinit var appSettingsRepository: AppSettingsRepository
@Inject
lateinit var appGpgSigner: AppGpgSigner
@Inject
lateinit var appEnvInfo: AppEnvInfo
@ -106,6 +111,8 @@ class App {
tabsManager.loadPersistedTabs()
GpgSigner.setDefault(appGpgSigner)
if (dirToOpen != null)
addDirTab(dirToOpen)
@ -267,7 +274,38 @@ class App {
if (currentTab != null) {
Column(
modifier = Modifier.background(MaterialTheme.colors.background)
modifier = Modifier
.background(MaterialTheme.colors.background)
.onPreviewKeyEvent {
when {
it.matchesBinding(KeybindingOption.OPEN_NEW_TAB) -> {
tabsManager.addNewEmptyTab()
true
}
it.matchesBinding(KeybindingOption.CLOSE_CURRENT_TAB) -> {
tabsManager.closeTab(currentTab)
true
}
it.matchesBinding(KeybindingOption.CHANGE_CURRENT_TAB_LEFT) -> {
val tabToSelect = tabs.getOrNull(tabs.indexOf(currentTab) - 1)
if (tabToSelect != null) {
tabsManager.selectTab(tabToSelect)
}
true
}
it.matchesBinding(KeybindingOption.CHANGE_CURRENT_TAB_RIGHT) -> {
val tabToSelect = tabs.getOrNull(tabs.indexOf(currentTab) + 1)
if (tabToSelect != null) {
tabsManager.selectTab(tabToSelect)
}
true
}
else -> false
}
}
) {
Tabs(
tabsInformationList = tabs,

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.0-beta01"
const val APP_VERSION_CODE = 12
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"
}

View File

@ -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

View File

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

View File

@ -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"

View File

@ -4,7 +4,10 @@ import com.jetpackduba.gitnuro.credentials.GpgCredentialsProvider
import org.bouncycastle.openpgp.PGPException
import org.eclipse.jgit.api.errors.CanceledException
import org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSigner
import org.eclipse.jgit.lib.*
import org.eclipse.jgit.lib.CommitBuilder
import org.eclipse.jgit.lib.GpgConfig
import org.eclipse.jgit.lib.ObjectBuilder
import org.eclipse.jgit.lib.PersonIdent
import org.eclipse.jgit.transport.CredentialsProvider
import javax.inject.Inject
import javax.inject.Provider
@ -12,57 +15,47 @@ import javax.inject.Provider
private const val INVALID_PASSWORD_MESSAGE = "Is the entered passphrase correct?"
class AppGpgSigner @Inject constructor(
private val gpgCredentials: GpgCredentialsProvider,
private val gpgCredentialsProvider: Provider<GpgCredentialsProvider>,
) : BouncyCastleGpgSigner() {
override fun sign(
repository: Repository?,
config: GpgConfig?,
data: ByteArray?,
committer: PersonIdent?,
signingKey: String?,
commit: CommitBuilder,
gpgSigningKey: String,
committer: PersonIdent,
credentialsProvider: CredentialsProvider?
): GpgSignature {
return try {
var gpgSignature: GpgSignature? = null
retryIfWrongPassphrase { isRetry ->
gpgCredentials.isRetry = isRetry
gpgSignature = super.sign(repository, config, data, committer, signingKey, gpgCredentials)
gpgCredentials.savePasswordInMemory()
}
gpgSignature!!
} catch (ex: CanceledException) {
println("Signing cancelled")
throw ex
}
) {
super.sign(commit, gpgSigningKey, committer, gpgCredentialsProvider.get())
}
override fun canLocateSigningKey(
repository: Repository?,
config: GpgConfig?,
committer: PersonIdent?,
signingKey: String?,
gpgSigningKey: String,
committer: PersonIdent,
credentialsProvider: CredentialsProvider?
): Boolean {
return super.canLocateSigningKey(repository, config, committer, signingKey, gpgCredentials)
return super.canLocateSigningKey(gpgSigningKey, committer, gpgCredentialsProvider.get())
}
override fun canLocateSigningKey(
gpgSigningKey: String,
committer: PersonIdent,
credentialsProvider: CredentialsProvider?,
config: GpgConfig?
): Boolean {
return super.canLocateSigningKey(gpgSigningKey, committer, gpgCredentialsProvider.get(), config)
}
override fun signObject(
repository: Repository?,
config: GpgConfig?,
`object`: ObjectBuilder?,
committer: PersonIdent?,
signingKey: String?,
credentialsProvider: CredentialsProvider?
`object`: ObjectBuilder,
gpgSigningKey: String?,
committer: PersonIdent,
credentialsProvider: CredentialsProvider?,
config: GpgConfig?
) {
val gpgCredentialsProvider = gpgCredentials
val gpgCredentialsProvider = gpgCredentialsProvider.get()
try {
retryIfWrongPassphrase { isRetry ->
gpgCredentialsProvider.isRetry = isRetry
super.signObject(repository, config, `object`, committer, signingKey, credentialsProvider)
super.signObject(`object`, gpgSigningKey, committer, gpgCredentialsProvider, config)
gpgCredentialsProvider.savePasswordInMemory()
}
} catch (ex: CanceledException) {

View File

@ -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
}

View File

@ -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)

View File

@ -1,6 +1,5 @@
package com.jetpackduba.gitnuro.git.branches
import com.jetpackduba.gitnuro.exceptions.ConflictsException
import com.jetpackduba.gitnuro.exceptions.UncommittedChangesDetectedException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

View File

@ -9,19 +9,23 @@ import javax.inject.Inject
class SetTrackingBranchUseCase @Inject constructor() {
operator fun invoke(git: Git, ref: Ref, remoteName: String?, remoteBranch: Ref?) {
invoke(git, ref.simpleName, remoteName, remoteBranch?.simpleName)
}
operator fun invoke(git: Git, refName: String, remoteName: String?, remoteBranchName: String?) {
val repository: Repository = git.repository
val config: StoredConfig = repository.config
if (remoteName == null || remoteBranch == null) {
config.unset("branch", ref.simpleName, "remote")
config.unset("branch", ref.simpleName, "merge")
if (remoteName == null || remoteBranchName == null) {
config.unset("branch", refName, "remote")
config.unset("branch", refName, "merge")
} else {
config.setString("branch", ref.simpleName, "remote", remoteName)
config.setString("branch", refName, "remote", remoteName)
config.setString(
"branch",
ref.simpleName,
refName,
"merge",
BranchesConstants.UPSTREAM_BRANCH_CONFIG_PREFIX + remoteBranch.simpleName
BranchesConstants.UPSTREAM_BRANCH_CONFIG_PREFIX + remoteBranchName
)
}

View File

@ -1,18 +1,18 @@
package com.jetpackduba.gitnuro.git.rebase
import com.jetpackduba.gitnuro.exceptions.ConflictsException
import com.jetpackduba.gitnuro.exceptions.UncommittedChangesDetectedException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.MergeResult
import org.eclipse.jgit.api.RebaseCommand
import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
typealias IsMultiStep = Boolean
class RebaseBranchUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, ref: Ref) = withContext(Dispatchers.IO) {
suspend operator fun invoke(git: Git, ref: Ref): IsMultiStep = withContext(Dispatchers.IO) {
val rebaseResult = git.rebase()
.setOperation(RebaseCommand.Operation.BEGIN)
.setUpstream(ref.objectId)
@ -22,10 +22,10 @@ class RebaseBranchUseCase @Inject constructor() {
throw UncommittedChangesDetectedException("Rebase failed, the repository contains uncommitted changes.")
}
when (rebaseResult.status) {
RebaseResult.Status.UNCOMMITTED_CHANGES -> throw UncommittedChangesDetectedException("Merge failed, makes sure you repository doesn't contain uncommitted changes.")
RebaseResult.Status.CONFLICTS -> throw ConflictsException("Rebase produced conflicts, please fix them to continue.")
else -> {}
if (rebaseResult.status == RebaseResult.Status.UNCOMMITTED_CHANGES) {
throw UncommittedChangesDetectedException("Merge failed, makes sure you repository doesn't contain uncommitted changes.")
}
return@withContext rebaseResult.status == RebaseResult.Status.STOPPED || rebaseResult.status == RebaseResult.Status.CONFLICTS
}
}

View File

@ -12,7 +12,7 @@ class HandleTransportUseCase @Inject constructor(
private val sessionManager: GSessionManager,
private val httpCredentialsProvider: HttpCredentialsFactory,
) {
suspend operator fun invoke(git: Git?, block: suspend CredentialsHandler.() -> Unit) {
suspend operator fun <R> invoke(git: Git?, block: suspend CredentialsHandler.() -> R): R {
var cache: CredentialsCache? = null
val credentialsHandler = object : CredentialsHandler {
@ -37,8 +37,10 @@ class HandleTransportUseCase @Inject constructor(
}
}
credentialsHandler.block()
val result = credentialsHandler.block()
cache?.cacheCredentialsIfNeeded()
return result
}
}

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.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)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -1,6 +1,7 @@
package com.jetpackduba.gitnuro.git.remote_operations
import com.jetpackduba.gitnuro.git.branches.GetTrackingBranchUseCase
import com.jetpackduba.gitnuro.git.branches.SetTrackingBranchUseCase
import com.jetpackduba.gitnuro.git.branches.TrackingBranch
import com.jetpackduba.gitnuro.git.isRejected
import com.jetpackduba.gitnuro.git.statusMessage
@ -14,24 +15,37 @@ import org.eclipse.jgit.lib.ProgressMonitor
import org.eclipse.jgit.transport.RefLeaseSpec
import org.eclipse.jgit.transport.RefSpec
import javax.inject.Inject
import kotlin.math.max
class PushBranchUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase,
private val getTrackingBranchUseCase: GetTrackingBranchUseCase,
private val setTrackingBranchUseCase: SetTrackingBranchUseCase,
private val appSettingsRepository: AppSettingsRepository,
) {
// TODO This use case should also set the tracking branch to the new remote branch
suspend operator fun invoke(git: Git, force: Boolean, pushTags: Boolean) = withContext(Dispatchers.IO) {
val currentBranch = git.repository.fullBranch
val tracking = getTrackingBranchUseCase(git, git.repository.branch)
val currentBranch = git.repository.branch
val fullCurrentBranch = git.repository.fullBranch
val tracking = getTrackingBranchUseCase(git, currentBranch)
val refSpecStr = if (tracking != null) {
"$currentBranch:${Constants.R_HEADS}${tracking.branch}"
"$fullCurrentBranch:${Constants.R_HEADS}${tracking.branch}"
} else {
currentBranch
fullCurrentBranch
}
handleTransportUseCase(git) {
val remoteRefUpdate = handleTransportUseCase(git) {
push(git, tracking, refSpecStr, force, pushTags)
}
if (tracking == null && remoteRefUpdate != null) {
// [remoteRefUpdate.trackingRefUpdate.localName] should have the following format: refs/remotes/REMOTE_NAME/BRANCH_NAME
val remoteBranchPathSplit = remoteRefUpdate.trackingRefUpdate.localName.split("/")
val remoteName = remoteBranchPathSplit.getOrNull(2)
val remoteBranchName = remoteBranchPathSplit.takeLast(max(0, remoteBranchPathSplit.count() - 3)).joinToString("/")
setTrackingBranchUseCase(git, currentBranch, remoteName, remoteBranchName)
}
}
private suspend fun CredentialsHandler.push(
@ -117,5 +131,7 @@ class PushBranchUseCase @Inject constructor(
throw Exception(error.toString())
}
return@withContext pushResult.firstOrNull()?.remoteUpdates?.firstOrNull()
}
}

View File

@ -1,7 +1,6 @@
package com.jetpackduba.gitnuro.git.workspace
import com.jetpackduba.gitnuro.extensions.isMerging
import com.jetpackduba.gitnuro.git.AppGpgSigner
import com.jetpackduba.gitnuro.git.author.LoadAuthorUseCase
import com.jetpackduba.gitnuro.git.config.LoadSignOffConfigUseCase
import com.jetpackduba.gitnuro.git.config.LocalConfigConstants
@ -16,8 +15,7 @@ import javax.inject.Inject
class DoCommitUseCase @Inject constructor(
private val loadSignOffConfigUseCase: LoadSignOffConfigUseCase,
private val loadAuthorUseCase: LoadAuthorUseCase,
private val getRepositoryStateUseCase: GetRepositoryStateUseCase,
private val appGpgSigner: AppGpgSigner,
private val getRepositoryStateUseCase: GetRepositoryStateUseCase
) {
suspend operator fun invoke(
git: Git,
@ -25,7 +23,6 @@ class DoCommitUseCase @Inject constructor(
amend: Boolean,
author: PersonIdent?,
): RevCommit = withContext(Dispatchers.IO) {
val signOffConfig = loadSignOffConfigUseCase(git.repository)
val finalMessage = if (signOffConfig.isEnabled) {
@ -43,7 +40,6 @@ class DoCommitUseCase @Inject constructor(
val isMerging = state.isMerging
git.commit()
.setSigner(appGpgSigner)
.setMessage(finalMessage)
.setAllowEmpty(amend || isMerging) // Only allow empty commits when amending
.setAmend(amend)

View File

@ -69,9 +69,29 @@ enum class KeybindingOption {
STASH_POP,
/**
* Used to pop stash changes to workspace
* Used to open a repository
*/
OPEN_ANOTHER_REPOSITORY,
OPEN_REPOSITORY,
/**
* Used to open a new tab
*/
OPEN_NEW_TAB,
/**
* Used to close current tab
*/
CLOSE_CURRENT_TAB,
/**
* Used to change current tab to the one in the left
*/
CHANGE_CURRENT_TAB_LEFT,
/**
* Used to change current tab to the one in the right
*/
CHANGE_CURRENT_TAB_RIGHT,
}
@ -111,9 +131,21 @@ private fun baseKeybindings() = mapOf(
KeybindingOption.STASH_POP to listOf(
Keybinding(key = Key.S, control = true, shift = true),
),
KeybindingOption.OPEN_ANOTHER_REPOSITORY to listOf(
KeybindingOption.OPEN_REPOSITORY to listOf(
Keybinding(key = Key.O, control = true),
),
KeybindingOption.OPEN_NEW_TAB to listOf(
Keybinding(key = Key.T, control = true),
),
KeybindingOption.CLOSE_CURRENT_TAB to listOf(
Keybinding(key = Key.W, control = true),
),
KeybindingOption.CHANGE_CURRENT_TAB_LEFT to listOf(
Keybinding(key = Key.DirectionLeft, alt = true),
),
KeybindingOption.CHANGE_CURRENT_TAB_RIGHT to listOf(
Keybinding(key = Key.DirectionRight, alt = true),
),
)
private fun linuxKeybindings(): Map<KeybindingOption, List<Keybinding>> = baseKeybindings()

View File

@ -94,7 +94,7 @@ fun AppTab(
is RepositorySelectionStatus.Open -> {
RepositoryOpenPage(
repositoryOpenViewModel = tabViewModel.repositoryOpenViewModel,
repositoryOpenViewModel = repositorySelectionStatusValue.viewModel,
onShowSettingsDialog = { showSettingsDialog = true },
onShowCloneDialog = { showCloneDialog = true },
)

View File

@ -153,7 +153,7 @@ fun RepositoryOpenPage(
true
}
it.matchesBinding(KeybindingOption.OPEN_ANOTHER_REPOSITORY) -> {
it.matchesBinding(KeybindingOption.OPEN_REPOSITORY) -> {
showOpenPopup = true
true
}

View File

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

View File

@ -25,7 +25,9 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.key.*
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@ -119,11 +121,24 @@ fun WelcomeView(
) {
var showAdditionalInfo by remember { mutableStateOf(false) }
val searchFocusRequester = remember { FocusRequester() }
val welcomeViewFocusRequester = remember { FocusRequester() }
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.surface),
.focusable(true)
.focusRequester(welcomeViewFocusRequester)
.background(MaterialTheme.colors.surface)
.onPreviewKeyEvent {
when {
it.matchesBinding(KeybindingOption.OPEN_REPOSITORY) -> {
onOpenRepository()
true
}
else -> false
}
},
) {
Column(
@ -156,6 +171,7 @@ fun WelcomeView(
canRepositoriesBeRemoved = true,
onOpenKnownRepository = onOpenKnownRepository,
onRemoveRepositoryFromRecent = onRemoveRepositoryFromRecent,
searchFieldFocusRequester = searchFocusRequester,
)
}
}
@ -173,6 +189,14 @@ fun WelcomeView(
)
}
LaunchedEffect(recentlyOpenedRepositories.isEmpty()) {
if (recentlyOpenedRepositories.isEmpty()) {
welcomeViewFocusRequester.requestFocus()
} else {
searchFocusRequester.requestFocus()
}
}
if (showAdditionalInfo) {
AppInfoDialog(
onClose = { showAdditionalInfo = false },
@ -287,6 +311,7 @@ fun RecentRepositories(
canRepositoriesBeRemoved: Boolean,
onRemoveRepositoryFromRecent: (String) -> Unit,
onOpenKnownRepository: (String) -> Unit,
searchFieldFocusRequester: FocusRequester,
) {
Column(
modifier = Modifier
@ -307,6 +332,7 @@ fun RecentRepositories(
canRepositoriesBeRemoved = canRepositoriesBeRemoved,
onRemoveRepositoryFromRecent = onRemoveRepositoryFromRecent,
onOpenKnownRepository = onOpenKnownRepository,
searchFieldFocusRequester = searchFieldFocusRequester,
)
}
}
@ -316,7 +342,7 @@ fun RecentRepositories(
fun RecentRepositoriesList(
recentlyOpenedRepositories: List<String>,
canRepositoriesBeRemoved: Boolean,
searchFieldFocusRequester: FocusRequester = remember { FocusRequester() },
searchFieldFocusRequester: FocusRequester,
onRemoveRepositoryFromRecent: (String) -> Unit,
onOpenKnownRepository: (String) -> Unit,
) {
@ -346,7 +372,7 @@ fun RecentRepositoriesList(
return@onPreviewKeyEvent false
}
when {
when {
it.matchesBinding(KeybindingOption.DOWN) -> {
if (focusedItemIndex < filteredRepositories.lastIndex) {
focusedItemIndex += 1

View File

@ -21,7 +21,6 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.di.AppComponent

View File

@ -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)

View File

@ -390,7 +390,7 @@ fun Terminal(settingsViewModel: SettingsViewModel) {
fun Logs(settingsViewModel: SettingsViewModel) {
SettingButton(
title = "Logs",
subtitle = "View the logs folder",
subtitle = "Open the logs folder",
buttonText = "Open folder",
onClick = {
settingsViewModel.openLogsFolderInFileExplorer()

View File

@ -0,0 +1,19 @@
package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.models.AuthorInfoSimple
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import javax.inject.Inject
class GetAuthorInfoUseCase @Inject constructor() {
suspend operator fun invoke(git: Git) = withContext(Dispatchers.IO) {
val config = git.repository.config
config.load()
val userName = config.getString("user", null, "name")
val email = config.getString("user", null, "email")
AuthorInfoSimple(userName, email)
}
}

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.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(

View File

@ -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,

View File

@ -2,16 +2,10 @@ 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.exceptions.codeToMessage
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
@ -31,16 +25,20 @@ 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.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.isActive
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
@ -61,6 +59,7 @@ class RepositoryOpenViewModel @Inject constructor(
private val tabState: TabState,
val appStateManager: AppStateManager,
private val fileChangesWatcher: FileChangesWatcher,
private val getAuthorInfoUseCase: GetAuthorInfoUseCase,
private val createBranchUseCase: CreateBranchUseCase,
private val stashChangesUseCase: StashChangesUseCase,
private val stageUntrackedFileUseCase: StageUntrackedFileUseCase,
@ -111,6 +110,8 @@ class RepositoryOpenViewModel @Inject constructor(
var authorViewModel: AuthorViewModel? = null
private set
private var hasGitDirChanged = false
init {
tabScope.run {
launch {
@ -120,7 +121,7 @@ class RepositoryOpenViewModel @Inject constructor(
}
launch {
watchRepositoryChanges(tabState.git)
watchRepositoryChanges()
}
}
}
@ -138,13 +139,8 @@ class RepositoryOpenViewModel @Inject constructor(
tabsManager.addNewTabFromPath(directory, true, getWorkspacePathUseCase(git))
}
private fun loadAuthorInfo(git: Git) {
val config = git.repository.config
config.load()
val userName = config.getString("user", null, "name")
val email = config.getString("user", null, "email")
_authorInfoSimple.value = AuthorInfoSimple(userName, email)
private suspend fun loadAuthorInfo(git: Git) {
_authorInfoSimple.value = getAuthorInfoUseCase(git)
}
fun showAuthorInfoDialog() {
@ -164,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")
}
}

View File

@ -74,8 +74,10 @@ class SharedBranchesViewModel @Inject constructor(
taskType = TaskType.REBASE_BRANCH,
refreshEvenIfCrashes = true,
) { git ->
rebaseBranchUseCase(git, ref)
positiveNotification("\"${ref.simpleName}\" rebased")
if (rebaseBranchUseCase(git, ref)) {
warningNotification("Rebase produced conflicts, please fix them to continue.")
} else {
positiveNotification("\"${ref.simpleName}\" rebased")
}
}
}

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.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}\"")
}
}
}

View File

@ -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")

View File

@ -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
@ -75,8 +67,6 @@ class TabViewModel @Inject constructor(
val errorsManager: ErrorsManager = tabState.errorsManager
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
val repositoryOpenViewModel: RepositoryOpenViewModel = repositoryOpenViewModelProvider.get()
private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None)
val repositorySelectionStatus: StateFlow<RepositorySelectionStatus>
get() = _repositorySelectionStatus
@ -115,7 +105,6 @@ class TabViewModel @Inject constructor(
}
repository.workTree // test if repository is valid
_repositorySelectionStatus.value = RepositorySelectionStatus.Open(repository)
val path = if (directory.name == ".git") {
directory.parent
@ -126,6 +115,9 @@ class TabViewModel @Inject constructor(
val git = Git(repository)
tabState.initGit(git)
_repositorySelectionStatus.value = RepositorySelectionStatus.Open(repositoryOpenViewModelProvider.get())
tabState.refreshData(RefreshType.ALL_DATA)
} catch (ex: Exception) {
onRepositoryChanged(null)
@ -196,5 +188,5 @@ class TabViewModel @Inject constructor(
sealed class RepositorySelectionStatus {
data object None : RepositorySelectionStatus()
data class Opening(val path: String) : RepositorySelectionStatus()
data class Open(val repository: Repository) : RepositorySelectionStatus()
data class Open(val viewModel: RepositoryOpenViewModel) : RepositorySelectionStatus()
}