Compare commits

..

No commits in common. "main" and "1.4.0-RC01" have entirely different histories.

13 changed files with 88 additions and 91 deletions

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

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

@ -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-rc01"
const val APP_VERSION_CODE = 15 const val APP_VERSION_CODE = 13
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

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

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

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

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

@ -2,7 +2,7 @@ package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.SharedRepositoryStateManager import com.jetpackduba.gitnuro.SharedRepositoryStateManager
import com.jetpackduba.gitnuro.TaskType import com.jetpackduba.gitnuro.TaskType
import com.jetpackduba.gitnuro.exceptions.codeToMessage 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
@ -110,8 +110,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 +119,7 @@ class RepositoryOpenViewModel @Inject constructor(
} }
launch { launch {
watchRepositoryChanges() watchRepositoryChanges(tabState.git)
} }
} }
} }
@ -160,27 +158,11 @@ 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) {
var hasGitDirChanged = false
launch { launch {
fileChangesWatcher.changesNotifier.collect { watcherEvent -> fileChangesWatcher.changesNotifier.collect { latestUpdateChangedGitDir ->
when (watcherEvent) {
is WatcherEvent.RepositoryChanged -> repositoryChanged(watcherEvent.hasGitDirChanged)
is WatcherEvent.WatchInitError -> {
val message = codeToMessage(watcherEvent.code)
errorsManager.addError(
newErrorNow(
exception = Exception(message),
taskType = TaskType.CHANGES_DETECTION,
),
)
}
}
}
}
}
private suspend fun CoroutineScope.repositoryChanged(hasGitDirChanged: Boolean) {
val isOperationRunning = tabState.operationRunning val isOperationRunning = tabState.operationRunning
if (!isOperationRunning) { // Only update if there isn't any process running if (!isOperationRunning) { // Only update if there isn't any process running
@ -189,26 +171,44 @@ class RepositoryOpenViewModel @Inject constructor(
val currentTimeMillis = System.currentTimeMillis() val currentTimeMillis = System.currentTimeMillis()
if ( if (
hasGitDirChanged && latestUpdateChangedGitDir &&
currentTimeMillis - tabState.lastOperation < MIN_TIME_AFTER_GIT_OPERATION currentTimeMillis - tabState.lastOperation < MIN_TIME_AFTER_GIT_OPERATION
) { ) {
printDebug(TAG, "Git operation was executed recently, ignoring file system change") printDebug(TAG, "Git operation was executed recently, ignoring file system change")
return return@collect
} }
if (hasGitDirChanged) { if (latestUpdateChangedGitDir) {
this@RepositoryOpenViewModel.hasGitDirChanged = true hasGitDirChanged = true
} }
if (isActive) { if (isActive) {
updateApp(hasGitDirChanged) updateApp(hasGitDirChanged)
} }
this@RepositoryOpenViewModel.hasGitDirChanged = false hasGitDirChanged = false
} else { } else {
printDebug(TAG, "Ignored file events during operation") 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 updateApp(hasGitDirChanged: Boolean) { private suspend fun updateApp(hasGitDirChanged: Boolean) {
if (hasGitDirChanged) { if (hasGitDirChanged) {

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