Compare commits

..

1 Commits

Author SHA1 Message Date
Abdelilah El Aissaoui
4c42f796ce Updated JGit to 7.0.0 2024-09-13 23:17:48 +02:00
38 changed files with 302 additions and 419 deletions

View File

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

View File

@ -8,8 +8,6 @@
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" id("org.jetbrains.compose") version "1.7.0-beta02"
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.1" val projectVersion = "1.4.0-beta01"
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.1" val projectVersionSimplified = "1.4.0"
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"
@ -47,7 +47,7 @@ repositories {
} }
dependencies { dependencies {
val jgit = "6.9.0.202403050737-r" val jgit = "7.0.0.202409031743-r"
if (currentOs() == OS.LINUX && isLinuxAarch64) { if (currentOs() == OS.LINUX && isLinuxAarch64) {
implementation(compose.desktop.linux_arm64) implementation(compose.desktop.linux_arm64)

View File

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

View File

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

View File

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

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.1" const val APP_VERSION = "1.4.0-beta01"
const val APP_VERSION_CODE = 15 const val APP_VERSION_CODE = 12
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,7 +2,6 @@ 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
@ -260,8 +259,7 @@ 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") {
printError(TAG, "Invalid credentials helper: \"$credentialHelperPath\" is not yet supported") throw NotSupportedHelper("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

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

View File

@ -1,6 +1,11 @@
package com.jetpackduba.gitnuro.exceptions package com.jetpackduba.gitnuro.exceptions
fun codeToMessage(code: Int): String { class WatcherInitException(
code: Int,
message: String = codeToMessage(code),
) : GitnuroException(message)
private fun codeToMessage(code: Int): String {
return when (code) { 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

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

View File

@ -3,6 +3,7 @@ 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
@ -24,8 +25,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<WatcherEvent>() private val _changesNotifier = MutableSharedFlow<Boolean>()
val changesNotifier: SharedFlow<WatcherEvent> = _changesNotifier val changesNotifier: SharedFlow<Boolean> = _changesNotifier
private val fileWatcher = FileWatcher.new() private val fileWatcher = FileWatcher.new()
private var shouldKeepLooping = true private var shouldKeepLooping = true
@ -70,16 +71,14 @@ class FileChangesWatcher @Inject constructor(
if (!areAllPathsIgnored) { if (!areAllPathsIgnored) {
println("Emitting changes $hasGitIgnoreChanged") println("Emitting changes $hasGitIgnoreChanged")
_changesNotifier.emit(WatcherEvent.RepositoryChanged(hasGitDirChanged)) _changesNotifier.emit(hasGitDirChanged)
} }
} }
} }
override fun onError(code: Int) { override fun onError(code: Int) {
tabScope.launch { throw WatcherInitException(code)
_changesNotifier.emit(WatcherEvent.WatchInitError(code))
}
} }
} }
@ -92,8 +91,3 @@ class FileChangesWatcher @Inject constructor(
} }
} }
sealed interface WatcherEvent {
data class RepositoryChanged(val hasGitDirChanged: Boolean) : WatcherEvent
data class WatchInitError(val code: Int) : WatcherEvent
}

View File

@ -15,12 +15,9 @@ class SaveAuthorUseCase @Inject constructor() {
repoConfig.load() repoConfig.load()
if (globalConfig is FileBasedConfig) { if (globalConfig is FileBasedConfig) {
val canonicalConfigFile = globalConfig.file.canonicalFile globalConfig.setStringProperty("user", null, "name", newAuthorInfo.globalName)
val globalRepoConfig = FileBasedConfig(canonicalConfigFile, git.repository.fs) globalConfig.setStringProperty("user", null, "email", newAuthorInfo.globalEmail)
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

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

View File

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

View File

@ -1,18 +1,18 @@
package com.jetpackduba.gitnuro.git.rebase package com.jetpackduba.gitnuro.git.rebase
import com.jetpackduba.gitnuro.exceptions.ConflictsException
import com.jetpackduba.gitnuro.exceptions.UncommittedChangesDetectedException import com.jetpackduba.gitnuro.exceptions.UncommittedChangesDetectedException
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.MergeResult
import org.eclipse.jgit.api.RebaseCommand import org.eclipse.jgit.api.RebaseCommand
import org.eclipse.jgit.api.RebaseResult import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
import javax.inject.Inject import javax.inject.Inject
typealias IsMultiStep = Boolean
class RebaseBranchUseCase @Inject constructor() { class RebaseBranchUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, ref: Ref): IsMultiStep = withContext(Dispatchers.IO) { suspend operator fun invoke(git: Git, ref: Ref) = withContext(Dispatchers.IO) {
val rebaseResult = git.rebase() val rebaseResult = git.rebase()
.setOperation(RebaseCommand.Operation.BEGIN) .setOperation(RebaseCommand.Operation.BEGIN)
.setUpstream(ref.objectId) .setUpstream(ref.objectId)
@ -22,10 +22,10 @@ class RebaseBranchUseCase @Inject constructor() {
throw UncommittedChangesDetectedException("Rebase failed, the repository contains uncommitted changes.") throw UncommittedChangesDetectedException("Rebase failed, the repository contains uncommitted changes.")
} }
if (rebaseResult.status == RebaseResult.Status.UNCOMMITTED_CHANGES) { when (rebaseResult.status) {
throw UncommittedChangesDetectedException("Merge failed, makes sure you repository doesn't contain uncommitted changes.") 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 -> {}
} }
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 sessionManager: GSessionManager,
private val httpCredentialsProvider: HttpCredentialsFactory, private val httpCredentialsProvider: HttpCredentialsFactory,
) { ) {
suspend operator fun <R> invoke(git: Git?, block: suspend CredentialsHandler.() -> R): R { suspend operator fun invoke(git: Git?, block: suspend CredentialsHandler.() -> Unit) {
var cache: CredentialsCache? = null var cache: CredentialsCache? = null
val credentialsHandler = object : CredentialsHandler { val credentialsHandler = object : CredentialsHandler {
@ -37,10 +37,8 @@ class HandleTransportUseCase @Inject constructor(
} }
} }
val result = credentialsHandler.block() credentialsHandler.block()
cache?.cacheCredentialsIfNeeded() cache?.cacheCredentialsIfNeeded()
return result
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
package com.jetpackduba.gitnuro.git.remote_operations package com.jetpackduba.gitnuro.git.remote_operations
import com.jetpackduba.gitnuro.git.branches.GetTrackingBranchUseCase 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.branches.TrackingBranch
import com.jetpackduba.gitnuro.git.isRejected import com.jetpackduba.gitnuro.git.isRejected
import com.jetpackduba.gitnuro.git.statusMessage import com.jetpackduba.gitnuro.git.statusMessage
@ -15,37 +14,24 @@ import org.eclipse.jgit.lib.ProgressMonitor
import org.eclipse.jgit.transport.RefLeaseSpec import org.eclipse.jgit.transport.RefLeaseSpec
import org.eclipse.jgit.transport.RefSpec import org.eclipse.jgit.transport.RefSpec
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max
class PushBranchUseCase @Inject constructor( class PushBranchUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase, private val handleTransportUseCase: HandleTransportUseCase,
private val getTrackingBranchUseCase: GetTrackingBranchUseCase, private val getTrackingBranchUseCase: GetTrackingBranchUseCase,
private val setTrackingBranchUseCase: SetTrackingBranchUseCase,
private val appSettingsRepository: AppSettingsRepository, 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) { suspend operator fun invoke(git: Git, force: Boolean, pushTags: Boolean) = withContext(Dispatchers.IO) {
val currentBranch = git.repository.branch val currentBranch = git.repository.fullBranch
val fullCurrentBranch = git.repository.fullBranch val tracking = getTrackingBranchUseCase(git, git.repository.branch)
val tracking = getTrackingBranchUseCase(git, currentBranch)
val refSpecStr = if (tracking != null) { val refSpecStr = if (tracking != null) {
"$fullCurrentBranch:${Constants.R_HEADS}${tracking.branch}" "$currentBranch:${Constants.R_HEADS}${tracking.branch}"
} else { } else {
fullCurrentBranch currentBranch
} }
handleTransportUseCase(git) {
val remoteRefUpdate = handleTransportUseCase(git) {
push(git, tracking, refSpecStr, force, pushTags) 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( private suspend fun CredentialsHandler.push(
@ -131,7 +117,5 @@ class PushBranchUseCase @Inject constructor(
throw Exception(error.toString()) throw Exception(error.toString())
} }
return@withContext pushResult.firstOrNull()?.remoteUpdates?.firstOrNull()
} }
} }

View File

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

View File

@ -69,29 +69,9 @@ enum class KeybindingOption {
STASH_POP, STASH_POP,
/** /**
* Used to open a repository * Used to pop stash changes to workspace
*/ */
OPEN_REPOSITORY, OPEN_ANOTHER_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,
} }
@ -131,21 +111,9 @@ private fun baseKeybindings() = mapOf(
KeybindingOption.STASH_POP to listOf( KeybindingOption.STASH_POP to listOf(
Keybinding(key = Key.S, control = true, shift = true), Keybinding(key = Key.S, control = true, shift = true),
), ),
KeybindingOption.OPEN_REPOSITORY to listOf( KeybindingOption.OPEN_ANOTHER_REPOSITORY to listOf(
Keybinding(key = Key.O, control = true), 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() private fun linuxKeybindings(): Map<KeybindingOption, List<Keybinding>> = baseKeybindings()

View File

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

View File

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

View File

@ -89,6 +89,7 @@ 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

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

View File

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

View File

@ -32,9 +32,7 @@ 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

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

View File

@ -1,19 +0,0 @@
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,9 +9,10 @@ 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.models.warningNotification import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
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
@ -25,7 +26,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,
@ -33,6 +34,8 @@ 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,
@ -41,11 +44,9 @@ class GlobalMenuActionsViewModel @Inject constructor(
refreshEvenIfCrashes = true, refreshEvenIfCrashes = true,
taskType = TaskType.PULL, taskType = TaskType.PULL,
) { git -> ) { git ->
if (pullBranchUseCase(git, pullType)) { pullBranchUseCase(git, pullType)
warningNotification("Pull produced conflicts, fix them to continue")
} else { positiveNotification("Pull completed")
positiveNotification("Pull completed")
}
} }
override fun fetchAll() = tabState.safeProcessing( override fun fetchAll() = tabState.safeProcessing(

View File

@ -1,10 +1,25 @@
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,10 +2,16 @@ 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.codeToMessage 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.*
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
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.stash.StashChangesUseCase
import com.jetpackduba.gitnuro.git.workspace.StageUntrackedFileUseCase import com.jetpackduba.gitnuro.git.workspace.StageUntrackedFileUseCase
import com.jetpackduba.gitnuro.logging.printDebug import com.jetpackduba.gitnuro.logging.printDebug
@ -25,20 +31,16 @@ 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.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.*
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.Git
import org.eclipse.jgit.api.errors.CheckoutConflictException import org.eclipse.jgit.api.errors.CheckoutConflictException
import org.eclipse.jgit.blame.BlameResult import org.eclipse.jgit.blame.BlameResult
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.lib.RepositoryState
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import java.awt.Desktop import java.awt.Desktop
import java.io.File
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@ -59,7 +61,6 @@ class RepositoryOpenViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
val appStateManager: AppStateManager, val appStateManager: AppStateManager,
private val fileChangesWatcher: FileChangesWatcher, private val fileChangesWatcher: FileChangesWatcher,
private val getAuthorInfoUseCase: GetAuthorInfoUseCase,
private val createBranchUseCase: CreateBranchUseCase, private val createBranchUseCase: CreateBranchUseCase,
private val stashChangesUseCase: StashChangesUseCase, private val stashChangesUseCase: StashChangesUseCase,
private val stageUntrackedFileUseCase: StageUntrackedFileUseCase, private val stageUntrackedFileUseCase: StageUntrackedFileUseCase,
@ -110,8 +111,6 @@ 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 {
@ -121,7 +120,7 @@ class RepositoryOpenViewModel @Inject constructor(
} }
launch { launch {
watchRepositoryChanges() watchRepositoryChanges(tabState.git)
} }
} }
} }
@ -139,8 +138,13 @@ class RepositoryOpenViewModel @Inject constructor(
tabsManager.addNewTabFromPath(directory, true, getWorkspacePathUseCase(git)) tabsManager.addNewTabFromPath(directory, true, getWorkspacePathUseCase(git))
} }
private suspend fun loadAuthorInfo(git: Git) { private fun loadAuthorInfo(git: Git) {
_authorInfoSimple.value = getAuthorInfoUseCase(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)
} }
fun showAuthorInfoDialog() { fun showAuthorInfoDialog() {
@ -160,53 +164,55 @@ 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() = tabScope.launch(Dispatchers.IO) { private suspend fun watchRepositoryChanges(git: Git) = tabScope.launch(Dispatchers.IO) {
launch { var hasGitDirChanged = false
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,
),
)
launch {
fileChangesWatcher.changesNotifier.collect { latestUpdateChangedGitDir ->
val isOperationRunning = tabState.operationRunning
if (!isOperationRunning) { // Only update if there isn't any process running
printDebug(TAG, "Detected changes in the repository's directory")
val currentTimeMillis = System.currentTimeMillis()
if (
latestUpdateChangedGitDir &&
currentTimeMillis - tabState.lastOperation < MIN_TIME_AFTER_GIT_OPERATION
) {
printDebug(TAG, "Git operation was executed recently, ignoring file system change")
return@collect
} }
if (latestUpdateChangedGitDir) {
hasGitDirChanged = true
}
if (isActive) {
updateApp(hasGitDirChanged)
}
hasGitDirChanged = false
} else {
printDebug(TAG, "Ignored file events during operation")
} }
} }
} }
}
private suspend fun CoroutineScope.repositoryChanged(hasGitDirChanged: Boolean) { try {
val isOperationRunning = tabState.operationRunning fileChangesWatcher.watchDirectoryPath(
repository = git.repository,
if (!isOperationRunning) { // Only update if there isn't any process running )
printDebug(TAG, "Detected changes in the repository's directory") } catch (ex: WatcherInitException) {
val message = ex.message
val currentTimeMillis = System.currentTimeMillis() if (message != null) {
errorsManager.addError(
if ( newErrorNow(
hasGitDirChanged && exception = ex,
currentTimeMillis - tabState.lastOperation < MIN_TIME_AFTER_GIT_OPERATION taskType = TaskType.CHANGES_DETECTION,
) { ),
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,10 +74,8 @@ class SharedBranchesViewModel @Inject constructor(
taskType = TaskType.REBASE_BRANCH, taskType = TaskType.REBASE_BRANCH,
refreshEvenIfCrashes = true, refreshEvenIfCrashes = true,
) { git -> ) { git ->
if (rebaseBranchUseCase(git, ref)) { rebaseBranchUseCase(git, ref)
warningNotification("Rebase produced conflicts, please fix them to continue.")
} else { positiveNotification("\"${ref.simpleName}\" rebased")
positiveNotification("\"${ref.simpleName}\" rebased")
}
} }
} }

View File

@ -9,7 +9,6 @@ import com.jetpackduba.gitnuro.git.remote_operations.DeleteRemoteBranchUseCase
import com.jetpackduba.gitnuro.git.remote_operations.PullFromSpecificBranchUseCase import com.jetpackduba.gitnuro.git.remote_operations.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
@ -70,10 +69,12 @@ 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 ->
if (pullFromSpecificBranchUseCase(git = git, remoteBranch = branch)) { pullFromSpecificBranchUseCase(
warningNotification("Pull produced conflicts, fix them to continue") git = git,
} else { rebase = false,
positiveNotification("Pulled from \"${branch.simpleName}\"") remoteBranch = branch,
} )
positiveNotification("Pulled from \"${branch.simpleName}\"")
} }
} }

View File

@ -395,9 +395,7 @@ 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,37 +1,45 @@
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.git.FileChangesWatcher import com.jetpackduba.gitnuro.exceptions.WatcherInitException
import com.jetpackduba.gitnuro.git.ProcessingState import com.jetpackduba.gitnuro.git.*
import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.branches.CreateBranchUseCase
import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState
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.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.*
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
@ -67,6 +75,8 @@ class TabViewModel @Inject constructor(
val errorsManager: ErrorsManager = tabState.errorsManager val errorsManager: ErrorsManager = tabState.errorsManager
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
val repositoryOpenViewModel: RepositoryOpenViewModel = repositoryOpenViewModelProvider.get()
private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None) private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None)
val repositorySelectionStatus: StateFlow<RepositorySelectionStatus> val repositorySelectionStatus: StateFlow<RepositorySelectionStatus>
get() = _repositorySelectionStatus get() = _repositorySelectionStatus
@ -105,6 +115,7 @@ class TabViewModel @Inject constructor(
} }
repository.workTree // test if repository is valid repository.workTree // test if repository is valid
_repositorySelectionStatus.value = RepositorySelectionStatus.Open(repository)
val path = if (directory.name == ".git") { val path = if (directory.name == ".git") {
directory.parent directory.parent
@ -115,9 +126,6 @@ class TabViewModel @Inject constructor(
val git = Git(repository) val git = Git(repository)
tabState.initGit(git) tabState.initGit(git)
_repositorySelectionStatus.value = RepositorySelectionStatus.Open(repositoryOpenViewModelProvider.get())
tabState.refreshData(RefreshType.ALL_DATA) tabState.refreshData(RefreshType.ALL_DATA)
} catch (ex: Exception) { } catch (ex: Exception) {
onRepositoryChanged(null) onRepositoryChanged(null)
@ -188,5 +196,5 @@ class TabViewModel @Inject constructor(
sealed class RepositorySelectionStatus { sealed class RepositorySelectionStatus {
data object None : RepositorySelectionStatus() data object None : RepositorySelectionStatus()
data class Opening(val path: String) : RepositorySelectionStatus() data class Opening(val path: String) : RepositorySelectionStatus()
data class Open(val viewModel: RepositoryOpenViewModel) : RepositorySelectionStatus() data class Open(val repository: Repository) : RepositorySelectionStatus()
} }