diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt b/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt index 15900a7..466eef3 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt @@ -40,6 +40,7 @@ object AppIcons { const val NETWORK = "network.svg" const val OPEN = "open.svg" const val PALETTE = "palette.svg" + const val PASSWORD = "password.svg" const val PASTE = "paste.svg" const val PERSON = "person.svg" const val REFRESH = "refresh.svg" diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/CredentialsCacheRepository.kt b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/CredentialsCacheRepository.kt new file mode 100644 index 0000000..e2bb808 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/CredentialsCacheRepository.kt @@ -0,0 +1,105 @@ +package com.jetpackduba.gitnuro.credentials + +import com.jetpackduba.gitnuro.extensions.lockUse +import kotlinx.coroutines.sync.Mutex +import org.eclipse.jgit.util.Base64 +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject +import javax.inject.Singleton + +private const val KEY_LENGTH = 16 + +@Singleton +class CredentialsCacheRepository @Inject constructor() { + private val credentialsCached = mutableListOf() + private val credentialsLock = Mutex(false) + // having a random key to encrypt the password may help in case of a memory dump attack + private val encryptionKey = getRandomKey() + + fun getCachedHttpCredentials(url: String): CredentialsType.HttpCredentials? { + val credentials = credentialsCached.filterIsInstance().firstOrNull { + it.url == url + } + + return credentials?.copy(password = credentials.password.cipherDecrypt()) + } + + suspend fun cacheHttpCredentials(credentials: CredentialsType.HttpCredentials) { + cacheHttpCredentials(credentials.url, credentials.userName, credentials.password) + } + + suspend fun cacheHttpCredentials(url: String, userName: String, password: String) { + val passwordEncrypted = password.cipherEncrypt() + + credentialsLock.lockUse { + val previouslyCached = credentialsCached.any { + it is CredentialsType.HttpCredentials && it.url == url + } + + if (!previouslyCached) { + val credentials = CredentialsType.HttpCredentials(url, userName, passwordEncrypted) + credentialsCached.add(credentials) + } + } + } + + suspend fun cacheSshCredentials(sshKey: String, password: String) { + credentialsLock.lockUse { + val previouslyCached = credentialsCached.any { + it is CredentialsType.SshCredentials && it.sshKey == sshKey + } + + if (!previouslyCached) { + val credentials = CredentialsType.SshCredentials(sshKey, password) + credentialsCached.add(credentials) + } + } + } + + private fun String.cipherEncrypt(): String { + val secretKeySpec = SecretKeySpec(encryptionKey.toByteArray(), "AES") + val iv = encryptionKey.toByteArray() + val ivParameterSpec = IvParameterSpec(iv) + + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + + val encryptedValue = cipher.doFinal(this.toByteArray()) + return Base64.encodeBytes(encryptedValue) + } + + private fun String.cipherDecrypt(): String { + val secretKeySpec = SecretKeySpec(encryptionKey.toByteArray(), "AES") + val iv = encryptionKey.toByteArray() + val ivParameterSpec = IvParameterSpec(iv) + + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + + val decodedValue = Base64.decode(this) + val decryptedValue = cipher.doFinal(decodedValue) + return String(decryptedValue) + } + + private fun getRandomKey(): String { + val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') + return (1..KEY_LENGTH) + .map { allowedChars.random() } + .joinToString("") + } +} + +sealed interface CredentialsType { + data class SshCredentials( + val sshKey: String, + val password: String, + ) : CredentialsType + + data class HttpCredentials( + val url: String, + val userName: String, + val password: String, + ) : CredentialsType +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/GSessionManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/GSessionManager.kt index 77bc466..c35f06f 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/GSessionManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/GSessionManager.kt @@ -1,5 +1,6 @@ package com.jetpackduba.gitnuro.credentials +import com.jetpackduba.gitnuro.git.remote_operations.CredentialsCache import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.RemoteSession import org.eclipse.jgit.transport.SshSessionFactory @@ -9,26 +10,33 @@ import javax.inject.Inject import javax.inject.Provider class GSessionManager @Inject constructor( - private val sessionProvider: Provider + private val mySessionFactory: MySessionFactory, ) { - fun generateSshSessionFactory(): SshSessionFactory { - return object : SshSessionFactory() { - override fun getSession( - uri: URIish, - credentialsProvider: CredentialsProvider?, - fs: FS?, - tms: Int - ): RemoteSession { - val remoteSession = sessionProvider.get() - remoteSession.setup(uri) + fun generateSshSessionFactory(): MySessionFactory { + return mySessionFactory + } +} - return remoteSession - } +class MySessionFactory @Inject constructor( + private val sessionProvider: Provider +) : SshSessionFactory(), CredentialsCache { + override fun getSession( + uri: URIish, + credentialsProvider: CredentialsProvider?, + fs: FS?, + tms: Int + ): RemoteSession { + val remoteSession = sessionProvider.get() + remoteSession.setup(uri) - override fun getType(): String { - return "ssh" //TODO What should be the value of this? - } + return remoteSession + } - } + override fun getType(): String { + return "ssh" //TODO What should be the value of this? + } + + override suspend fun cacheCredentialsIfNeeded() { + // Nothing to do until we add some kind of password cache for SSHKeys } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt index ccae849..0239bd6 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt @@ -1,7 +1,9 @@ package com.jetpackduba.gitnuro.credentials import com.jetpackduba.gitnuro.exceptions.NotSupportedHelper +import com.jetpackduba.gitnuro.git.remote_operations.CredentialsCache import com.jetpackduba.gitnuro.managers.IShellManager +import com.jetpackduba.gitnuro.preferences.AppSettings import dagger.assisted.Assisted import dagger.assisted.AssistedInject import org.eclipse.jgit.api.Git @@ -18,8 +20,13 @@ private const val TIMEOUT_MIN = 1L class HttpCredentialsProvider @AssistedInject constructor( private val credentialsStateManager: CredentialsStateManager, private val shellManager: IShellManager, + private val appSettings: AppSettings, + private val credentialsCacheRepository: CredentialsCacheRepository, @Assisted val git: Git?, -) : CredentialsProvider() { +) : CredentialsProvider(), CredentialsCache { + + private var credentialsCached: CredentialsType.HttpCredentials? = null + override fun isInteractive(): Boolean { return true } @@ -45,15 +52,32 @@ class HttpCredentialsProvider @AssistedInject constructor( val externalCredentialsHelper = getExternalCredentialsHelper(uri, git) if (externalCredentialsHelper == null) { - val credentials = askForCredentials() + val cachedCredentials = credentialsCacheRepository.getCachedHttpCredentials(uri.toString()) - if (credentials is CredentialsAccepted.HttpCredentialsAccepted) { - userItem.value = credentials.user - passwordItem.value = credentials.password.toCharArray() + if (cachedCredentials == null) { + val credentials = askForCredentials() + + if (credentials is CredentialsAccepted.HttpCredentialsAccepted) { + userItem.value = credentials.user + passwordItem.value = credentials.password.toCharArray() + + if (appSettings.cacheCredentialsInMemory) { + credentialsCached = CredentialsType.HttpCredentials( + url = uri.toString(), + userName = credentials.user, + password = credentials.password, + ) + } + + return true + } else if (credentials is CredentialsState.CredentialsDenied) { + throw CancellationException("Credentials denied") + } + } else { + userItem.value = cachedCredentials.userName + passwordItem.value = cachedCredentials.password.toCharArray() return true - } else if (credentials is CredentialsState.CredentialsDenied) { - throw CancellationException("Credentials denied") } return false @@ -153,26 +177,26 @@ class HttpCredentialsProvider @AssistedInject constructor( } bufferedReader.use { - var line: String - while (bufferedReader.readLine().also { - line = checkNotNull(it) { "Cancelled authentication" } - } != null && !(usernameSet && passwordSet)) { - if (line.startsWith("username=")) { - val split = line.split("=") + var line: String? + while (bufferedReader.readLine().also { line = it } != null && !(usernameSet && passwordSet)) { + val safeLine = line ?: continue + + if (safeLine.startsWith("username=")) { + val split = safeLine.split("=") val userName = split.getOrNull(1) ?: return ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED - val userNameItem = items.firstOrNull { it.promptText == "Username" } + val userNameItem = items.firstOrNull { it is CredentialItem.Username } if (userNameItem is CredentialItem.Username) { userNameItem.value = userName usernameSet = true } - } else if (line.startsWith("password=")) { - val split = line.split("=") + } else if (safeLine.startsWith("password=")) { + val split = safeLine.split("=") val password = split.getOrNull(1) ?: return ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED - val passwordItem = items.firstOrNull { it.promptText == "Password" } + val passwordItem = items.firstOrNull { it is CredentialItem.Password } if (passwordItem is CredentialItem.Password) { passwordItem.value = password.toCharArray() @@ -182,9 +206,9 @@ class HttpCredentialsProvider @AssistedInject constructor( } } - return if (usernameSet && passwordSet) + return if (usernameSet && passwordSet) { ExternalCredentialsRequestResult.SUCCESS - else + } else ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED } @@ -206,7 +230,7 @@ class HttpCredentialsProvider @AssistedInject constructor( val genericCredentialHelper = config.getString("credential", null, "helper") val uriSpecificCredentialHelper = config.getString("credential", hostWithProtocol, "helper") - var credentialHelperPath = uriSpecificCredentialHelper ?: genericCredentialHelper ?: return null + val credentialHelperPath = uriSpecificCredentialHelper ?: genericCredentialHelper ?: return null if (credentialHelperPath == "cache" || credentialHelperPath == "store") { throw NotSupportedHelper("Invalid credentials helper: \"$credentialHelperPath\" is not yet supported") @@ -225,6 +249,12 @@ class HttpCredentialsProvider @AssistedInject constructor( return ExternalCredentialsHelper(credentialHelperPath, useHttpPath) } + + override suspend fun cacheCredentialsIfNeeded() { + credentialsCached?.let { + credentialsCacheRepository.cacheHttpCredentials(it) + } + } } data class ExternalCredentialsHelper( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt b/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt index ec71825..58a9ce2 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt @@ -2,6 +2,7 @@ package com.jetpackduba.gitnuro.di import com.jetpackduba.gitnuro.App import com.jetpackduba.gitnuro.AppEnvInfo +import com.jetpackduba.gitnuro.credentials.CredentialsCacheRepository import com.jetpackduba.gitnuro.credentials.CredentialsStateManager import com.jetpackduba.gitnuro.di.modules.AppModule import com.jetpackduba.gitnuro.di.modules.NetworkModule @@ -44,4 +45,6 @@ interface AppComponent { fun tempFilesManager(): TempFilesManager fun updatesRepository(): UpdatesRepository -} \ No newline at end of file + + fun credentialsCacheRepository(): CredentialsCacheRepository +} diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/extensions/MutexExtensions.kt b/src/main/kotlin/com/jetpackduba/gitnuro/extensions/MutexExtensions.kt new file mode 100644 index 0000000..eb841c6 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/extensions/MutexExtensions.kt @@ -0,0 +1,13 @@ +package com.jetpackduba.gitnuro.extensions + +import kotlinx.coroutines.sync.Mutex + +suspend fun Mutex.lockUse(block: () -> T): T { + this.lock() + + try { + return block() + } finally { + this.unlock() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/CloneRepositoryUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/CloneRepositoryUseCase.kt index 6117105..8788a7d 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/CloneRepositoryUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/CloneRepositoryUseCase.kt @@ -26,46 +26,47 @@ class CloneRepositoryUseCase @Inject constructor( try { ensureActive() trySend(CloneState.Cloning("Starting...", progress, lastTotalWork)) + handleTransportUseCase(null) { + Git.cloneRepository() + .setDirectory(directory) + .setURI(url) + .setProgressMonitor( + object : ProgressMonitor { + override fun start(totalTasks: Int) { + printDebug(TAG, "ProgressMonitor Start with total tasks of: $totalTasks") + } - Git.cloneRepository() - .setDirectory(directory) - .setURI(url) - .setProgressMonitor( - object : ProgressMonitor { - override fun start(totalTasks: Int) { - printDebug(TAG, "ProgressMonitor Start with total tasks of: $totalTasks") + override fun beginTask(title: String?, totalWork: Int) { + println("ProgressMonitor Begin task with title: $title") + lastTitle = title.orEmpty() + lastTotalWork = totalWork + progress = 0 + trySend(CloneState.Cloning(lastTitle, progress, lastTotalWork)) + } + + override fun update(completed: Int) { + printDebug(TAG, "ProgressMonitor Update $completed") + ensureActive() + + progress += completed + trySend(CloneState.Cloning(lastTitle, progress, lastTotalWork)) + } + + override fun endTask() { + printDebug(TAG, "ProgressMonitor End task") + } + + override fun isCancelled(): Boolean { + return !isActive + } + + override fun showDuration(enabled: Boolean) {} } - - override fun beginTask(title: String?, totalWork: Int) { - println("ProgressMonitor Begin task with title: $title") - lastTitle = title.orEmpty() - lastTotalWork = totalWork - progress = 0 - trySend(CloneState.Cloning(lastTitle, progress, lastTotalWork)) - } - - override fun update(completed: Int) { - printDebug(TAG, "ProgressMonitor Update $completed") - ensureActive() - - progress += completed - trySend(CloneState.Cloning(lastTitle, progress, lastTotalWork)) - } - - override fun endTask() { - printDebug(TAG, "ProgressMonitor End task") - } - - override fun isCancelled(): Boolean { - return !isActive - } - - override fun showDuration(enabled: Boolean) {} - } - ) - .setTransportConfigCallback { handleTransportUseCase(it, null) } - .setCloneSubmodules(cloneSubmodules) - .call() + ) + .setTransportConfigCallback { handleTransport(it) } + .setCloneSubmodules(cloneSubmodules) + .call() + } ensureActive() trySend(CloneState.Completed(directory)) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/DeleteRemoteBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/DeleteRemoteBranchUseCase.kt index db556fb..82a516a 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/DeleteRemoteBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/DeleteRemoteBranchUseCase.kt @@ -25,35 +25,33 @@ class DeleteRemoteBranchUseCase @Inject constructor( .setSource(null) .setDestination(branchName) - val pushResults = git.push() - .setTransportConfigCallback { - handleTransportUseCase(it, git) + handleTransportUseCase(git) { + val pushResults = git.push() + .setTransportConfigCallback { + handleTransport(it) + } + .setRefSpecs(refSpec) + .setRemote(remoteName) + .call() + + val results = pushResults.map { pushResult -> + pushResult.remoteUpdates.filter { remoteRefUpdate -> + remoteRefUpdate.status.isRejected + } + }.flatten() + + if (results.isNotEmpty()) { + val error = StringBuilder() + + results.forEach { result -> + error.append(result.statusMessage) + error.append("\n") + } + + throw Exception(error.toString()) } - .setRefSpecs(refSpec) - .setRemote(remoteName) - .call() - - val results = pushResults.map { pushResult -> - pushResult.remoteUpdates.filter { remoteRefUpdate -> - remoteRefUpdate.status.isRejected - } - }.flatten() - - if (results.isNotEmpty()) { - val error = StringBuilder() - - results.forEach { result -> - error.append(result.statusMessage) - error.append("\n") - } - - throw Exception(error.toString()) } - deleteBranchUseCase(git, ref) -// git -// .branchDelete() -// .setBranchNames(ref.name) -// .call() + } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/FetchAllBranchesUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/FetchAllBranchesUseCase.kt index 7ca1178..20a0215 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/FetchAllBranchesUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/FetchAllBranchesUseCase.kt @@ -23,26 +23,28 @@ class FetchAllBranchesUseCase @Inject constructor( val errors = mutableListOf>() for (remote in remotes) { try { - git.fetch() - .setRemote(remote.name) - .setRefSpecs(remote.fetchRefSpecs) - .setRemoveDeletedRefs(true) - .setTransportConfigCallback { handleTransportUseCase(it, git) } - .setCredentialsProvider(CredentialsProvider.getDefault()) - .setProgressMonitor(object : ProgressMonitor { - override fun start(totalTasks: Int) {} + handleTransportUseCase(git) { + git.fetch() + .setRemote(remote.name) + .setRefSpecs(remote.fetchRefSpecs) + .setRemoveDeletedRefs(true) + .setTransportConfigCallback { handleTransport(it) } + .setCredentialsProvider(CredentialsProvider.getDefault()) + .setProgressMonitor(object : ProgressMonitor { + override fun start(totalTasks: Int) {} - override fun beginTask(title: String?, totalWork: Int) {} + override fun beginTask(title: String?, totalWork: Int) {} - override fun update(completed: Int) {} + override fun update(completed: Int) {} - override fun endTask() {} + override fun endTask() {} - override fun isCancelled(): Boolean = isActive + override fun isCancelled(): Boolean = isActive - override fun showDuration(enabled: Boolean) {} - }) - .call() + override fun showDuration(enabled: Boolean) {} + }) + .call() + } } catch (ex: Exception) { printError(TAG, "Fetch failed for remote ${remote.name} with error ${ex.message}", ex) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/HandleTransportUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/HandleTransportUseCase.kt index 1d21167..bb6f558 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/HandleTransportUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/HandleTransportUseCase.kt @@ -12,11 +12,39 @@ class HandleTransportUseCase @Inject constructor( private val sessionManager: GSessionManager, private val httpCredentialsProvider: HttpCredentialsFactory, ) { - operator fun invoke(transport: Transport?, git: Git?) { - if (transport is SshTransport) { - transport.sshSessionFactory = sessionManager.generateSshSessionFactory() - } else if (transport is HttpTransport) { - transport.credentialsProvider = httpCredentialsProvider.create(git) + suspend operator fun invoke(git: Git?, block: suspend CredentialsHandler.() -> Unit) { + var cache: CredentialsCache? = null + + val credentialsHandler = object: CredentialsHandler { + override fun handleTransport(transport: Transport?) { + cache = when (transport) { + is SshTransport -> { + val sshSessionFactory = sessionManager.generateSshSessionFactory() + transport.sshSessionFactory = sshSessionFactory + sshSessionFactory + } + + is HttpTransport -> { + val httpCredentials = httpCredentialsProvider.create(git) + transport.credentialsProvider = httpCredentials + httpCredentials + } + + else -> { + null + } + } + } } + + credentialsHandler.block() + cache?.cacheCredentialsIfNeeded() } +} +interface CredentialsCache { + suspend fun cacheCredentialsIfNeeded() +} + +interface CredentialsHandler { + fun handleTransport(transport: Transport?) } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullBranchUseCase.kt index c99ef74..20d1dcb 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullBranchUseCase.kt @@ -4,6 +4,7 @@ import com.jetpackduba.gitnuro.preferences.AppSettings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.PullResult import org.eclipse.jgit.api.RebaseResult import org.eclipse.jgit.transport.CredentialsProvider import javax.inject.Inject @@ -19,25 +20,27 @@ class PullBranchUseCase @Inject constructor( PullType.DEFAULT -> appSettings.pullRebase } - val pullResult = git - .pull() - .setTransportConfigCallback { handleTransportUseCase(it, git) } - .setRebase(pullWithRebase) - .setCredentialsProvider(CredentialsProvider.getDefault()) - .call() + handleTransportUseCase(git) { + val pullResult = git + .pull() + .setTransportConfigCallback {this.handleTransport(it) } + .setRebase(pullWithRebase) + .setCredentialsProvider(CredentialsProvider.getDefault()) + .call() - if (!pullResult.isSuccessful) { - var message = "Pull failed" + 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 uncommited changes" - RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue" - else -> message + if (pullWithRebase) { + message = when (pullResult.rebaseResult.status) { + RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommited changes" + RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue" + else -> message + } } - } - throw Exception(message) + throw Exception(message) + } } } } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullFromSpecificBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullFromSpecificBranchUseCase.kt index db65faf..f86647f 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullFromSpecificBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullFromSpecificBranchUseCase.kt @@ -14,27 +14,30 @@ class PullFromSpecificBranchUseCase @Inject constructor( private val handleTransportUseCase: HandleTransportUseCase, ) { suspend operator fun invoke(git: Git, rebase: Boolean, remoteBranch: Ref) = withContext(Dispatchers.IO) { - val pullResult = git - .pull() - .setTransportConfigCallback { handleTransportUseCase(it, git) } - .setRemote(remoteBranch.remoteName) - .setRemoteBranchName(remoteBranch.simpleName) - .setRebase(rebase) - .setCredentialsProvider(CredentialsProvider.getDefault()) - .call() + handleTransportUseCase(git) { + val pullResult = git + .pull() + .setTransportConfigCallback { handleTransport(it) } + .setRemote(remoteBranch.remoteName) + .setRemoteBranchName(remoteBranch.simpleName) + .setRebase(rebase) + .setCredentialsProvider(CredentialsProvider.getDefault()) + .call() - if (!pullResult.isSuccessful) { - var message = "Pull failed" // TODO Remove messages from here and pass the result to a custom exception type + if (!pullResult.isSuccessful) { + var message = + "Pull failed" // TODO Remove messages from here and pass the result to a custom exception type - if (rebase) { - message = when (pullResult.rebaseResult.status) { - RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommited changes" - RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue" - else -> message + if (rebase) { + message = when (pullResult.rebaseResult.status) { + RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommited changes" + RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue" + else -> message + } } - } - throw Exception(message) + throw Exception(message) + } } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt index f7ab394..d8a6d19 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt @@ -1,6 +1,7 @@ package com.jetpackduba.gitnuro.git.remote_operations import com.jetpackduba.gitnuro.git.branches.GetTrackingBranchUseCase +import com.jetpackduba.gitnuro.git.branches.TrackingBranch import com.jetpackduba.gitnuro.git.isRejected import com.jetpackduba.gitnuro.git.statusMessage import com.jetpackduba.gitnuro.preferences.AppSettings @@ -27,7 +28,18 @@ class PushBranchUseCase @Inject constructor( } else { currentBranch } + handleTransportUseCase(git) { + push(git, tracking, refSpecStr, force, pushTags) + } + } + private suspend fun CredentialsHandler.push( + git: Git, + tracking: TrackingBranch?, + refSpecStr: String?, + force: Boolean, + pushTags: Boolean + ) = withContext(Dispatchers.IO) { val pushResult = git .push() .setRefSpecs(RefSpec(refSpecStr)) @@ -66,7 +78,7 @@ class PushBranchUseCase @Inject constructor( this } } - .setTransportConfigCallback { handleTransportUseCase(it, git) } + .setTransportConfigCallback { handleTransport(it) } .setProgressMonitor(object : ProgressMonitor { override fun start(totalTasks: Int) {} override fun beginTask(title: String?, totalWork: Int) {} @@ -105,5 +117,4 @@ class PushBranchUseCase @Inject constructor( throw Exception(error.toString()) } } - } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushToSpecificBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushToSpecificBranchUseCase.kt index af8b799..ea00650 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushToSpecificBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushToSpecificBranchUseCase.kt @@ -18,30 +18,32 @@ class PushToSpecificBranchUseCase @Inject constructor( withContext(Dispatchers.IO) { val currentBranchRefSpec = git.repository.fullBranch - val pushResult = git - .push() - .setRefSpecs(RefSpec("$currentBranchRefSpec:${remoteBranch.simpleName}")) - .setRemote(remoteBranch.remoteName) - .setForce(force) - .apply { - if (pushTags) - setPushTags() + handleTransportUseCase(git) { + val pushResult = git + .push() + .setRefSpecs(RefSpec("$currentBranchRefSpec:${remoteBranch.simpleName}")) + .setRemote(remoteBranch.remoteName) + .setForce(force) + .apply { + if (pushTags) + setPushTags() + } + .setTransportConfigCallback { handleTransport(it) } + .call() + + val results = + pushResult.map { it.remoteUpdates.filter { remoteRefUpdate -> remoteRefUpdate.status.isRejected } } + .flatten() + if (results.isNotEmpty()) { + val error = StringBuilder() + + results.forEach { result -> + error.append(result.statusMessage) + error.append("\n") + } + + throw Exception(error.toString()) } - .setTransportConfigCallback { handleTransportUseCase(it, git) } - .call() - - val results = - pushResult.map { it.remoteUpdates.filter { remoteRefUpdate -> remoteRefUpdate.status.isRejected } } - .flatten() - if (results.isNotEmpty()) { - val error = StringBuilder() - - results.forEach { result -> - error.append(result.statusMessage) - error.append("\n") - } - - throw Exception(error.toString()) } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/submodules/AddSubmoduleUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/submodules/AddSubmoduleUseCase.kt index d931d79..5ff4f24 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/submodules/AddSubmoduleUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/submodules/AddSubmoduleUseCase.kt @@ -13,22 +13,24 @@ class AddSubmoduleUseCase @Inject constructor( private val handleTransportUseCase: HandleTransportUseCase, ) { suspend operator fun invoke(git: Git, name: String, path: String, uri: String): Unit = withContext(Dispatchers.IO) { - git.submoduleAdd() - .setName(name) - .setPath(path) - .setURI(uri) - .setTransportConfigCallback { handleTransportUseCase(it, git) } - .setCredentialsProvider(CredentialsProvider.getDefault()) - .setProgressMonitor(object : ProgressMonitor { - override fun start(totalTasks: Int) {} - override fun beginTask(title: String?, totalWork: Int) {} - override fun update(completed: Int) {} - override fun endTask() {} - override fun showDuration(enabled: Boolean) {} + handleTransportUseCase(git) { + git.submoduleAdd() + .setName(name) + .setPath(path) + .setURI(uri) + .setTransportConfigCallback { handleTransport(it) } + .setCredentialsProvider(CredentialsProvider.getDefault()) + .setProgressMonitor(object : ProgressMonitor { + override fun start(totalTasks: Int) {} + override fun beginTask(title: String?, totalWork: Int) {} + override fun update(completed: Int) {} + override fun endTask() {} + override fun showDuration(enabled: Boolean) {} - override fun isCancelled() = !isActive + override fun isCancelled() = !isActive - }) - .call() + }) + .call() + } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/submodules/UpdateSubmoduleUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/submodules/UpdateSubmoduleUseCase.kt index b6881fa..760827f 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/submodules/UpdateSubmoduleUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/submodules/UpdateSubmoduleUseCase.kt @@ -16,32 +16,34 @@ class UpdateSubmoduleUseCase @Inject constructor( private val handleTransportUseCase: HandleTransportUseCase, ) { suspend operator fun invoke(git: Git, path: String) = withContext(Dispatchers.IO) { - git.submoduleUpdate() - .addPath(path) - .setCallback( - object : CloneCommand.Callback { - override fun initializedSubmodules(submodules: MutableCollection?) { + handleTransportUseCase(git) { + git.submoduleUpdate() + .addPath(path) + .setCallback( + object : CloneCommand.Callback { + override fun initializedSubmodules(submodules: MutableCollection?) { + } + + override fun cloningSubmodule(path: String?) { + + } + + override fun checkingOut(commit: AnyObjectId?, path: String?) { + + } } - - override fun cloningSubmodule(path: String?) { - - } - - override fun checkingOut(commit: AnyObjectId?, path: String?) { - - } - } - ) - .setTransportConfigCallback { handleTransportUseCase(it, git) } - .setProgressMonitor(object : ProgressMonitor { - override fun start(totalTasks: Int) {} - override fun beginTask(title: String?, totalWork: Int) {} - override fun update(completed: Int) {} - override fun endTask() {} - override fun isCancelled(): Boolean = !isActive - override fun showDuration(enabled: Boolean) {} - }) - .call() + ) + .setTransportConfigCallback { handleTransport(it) } + .setProgressMonitor(object : ProgressMonitor { + override fun start(totalTasks: Int) {} + override fun beginTask(title: String?, totalWork: Int) {} + override fun update(completed: Int) {} + override fun endTask() {} + override fun isCancelled(): Boolean = !isActive + override fun showDuration(enabled: Boolean) {} + }) + .call() + } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt b/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt index 91b675a..342cfd6 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt @@ -28,8 +28,9 @@ private const val PREF_CUSTOM_THEME = "customTheme" private const val PREF_UI_SCALE = "ui_scale" private const val PREF_DIFF_TYPE = "diffType" private const val PREF_DIFF_FULL_FILE = "diffFullFile" -private const val PREF_SWAP_UNCOMMITED_CHANGES = "inverseUncommitedChanges" +private const val PREF_SWAP_UNCOMMITTED_CHANGES = "inverseUncommittedChanges" private const val PREF_TERMINAL_PATH = "terminalPath" +private const val PREF_CACHE_CREDENTIALS_IN_MEMORY = "credentialsInMemory" private const val PREF_GIT_FF_MERGE = "gitFFMerge" @@ -38,7 +39,8 @@ private const val PREF_GIT_PUSH_WITH_LEASE = "gitPushWithLease" private const val DEFAULT_COMMITS_LIMIT = 1000 private const val DEFAULT_COMMITS_LIMIT_ENABLED = true -private const val DEFAULT_SWAP_UNCOMMITED_CHANGES = false +private const val DEFAULT_SWAP_UNCOMMITTED_CHANGES = false +private const val DEFAULT_CACHE_CREDENTIALS_IN_MEMORY = true const val DEFAULT_UI_SCALE = -1f @Singleton @@ -51,8 +53,11 @@ class AppSettings @Inject constructor() { private val _commitsLimitEnabledFlow = MutableStateFlow(commitsLimitEnabled) val commitsLimitEnabledFlow = _commitsLimitEnabledFlow.asStateFlow() - private val _swapUncommitedChangesFlow = MutableStateFlow(swapUncommitedChanges) - val swapUncommitedChangesFlow = _swapUncommitedChangesFlow.asStateFlow() + private val _swapUncommittedChangesFlow = MutableStateFlow(swapUncommittedChanges) + val swapUncommittedChangesFlow = _swapUncommittedChangesFlow.asStateFlow() + + private val _cacheCredentialsInMemoryFlow = MutableStateFlow(cacheCredentialsInMemory) + val cacheCredentialsInMemoryFlow = _cacheCredentialsInMemoryFlow.asStateFlow() private val _ffMergeFlow = MutableStateFlow(ffMerge) val ffMergeFlow = _ffMergeFlow.asStateFlow() @@ -123,13 +128,22 @@ class AppSettings @Inject constructor() { _commitsLimitEnabledFlow.value = value } - var swapUncommitedChanges: Boolean + var swapUncommittedChanges: Boolean get() { - return preferences.getBoolean(PREF_SWAP_UNCOMMITED_CHANGES, DEFAULT_SWAP_UNCOMMITED_CHANGES) + return preferences.getBoolean(PREF_SWAP_UNCOMMITTED_CHANGES, DEFAULT_SWAP_UNCOMMITTED_CHANGES) } set(value) { - preferences.putBoolean(PREF_SWAP_UNCOMMITED_CHANGES, value) - _swapUncommitedChangesFlow.value = value + preferences.putBoolean(PREF_SWAP_UNCOMMITTED_CHANGES, value) + _swapUncommittedChangesFlow.value = value + } + + var cacheCredentialsInMemory: Boolean + get() { + return preferences.getBoolean(PREF_CACHE_CREDENTIALS_IN_MEMORY, DEFAULT_CACHE_CREDENTIALS_IN_MEMORY) + } + set(value) { + preferences.putBoolean(PREF_CACHE_CREDENTIALS_IN_MEMORY, value) + _cacheCredentialsInMemoryFlow.value = value } var scaleUi: Float diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt index 65d324b..88aaad9 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* -import androidx.compose.material.Switch import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -17,7 +16,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.jetpackduba.gitnuro.AppIcons import com.jetpackduba.gitnuro.extensions.handMouseClickable -import com.jetpackduba.gitnuro.extensions.handOnHover import com.jetpackduba.gitnuro.managers.Error import com.jetpackduba.gitnuro.preferences.DEFAULT_UI_SCALE import com.jetpackduba.gitnuro.theme.* @@ -48,6 +46,7 @@ val settings = listOf( SettingsEntry.Section("Network"), SettingsEntry.Entry(AppIcons.NETWORK, "Proxy") { }, + SettingsEntry.Entry(AppIcons.PASSWORD, "Authentication") { Authentication(it) }, SettingsEntry.Section("Tools"), SettingsEntry.Entry(AppIcons.TERMINAL, "Terminal") { Terminal(it) }, @@ -240,6 +239,21 @@ private fun RemoteActions(settingsViewModel: SettingsViewModel) { ) } + +@Composable +private fun Authentication(settingsViewModel: SettingsViewModel) { + val cacheCredentialsInMemory by settingsViewModel.cacheCredentialsInMemoryFlow.collectAsState() + + SettingToggle( + title = "Cache HTTP credentials in memory", + subtitle = "If active, HTTP Credentials will be remember until Gitnuro is closed", + value = cacheCredentialsInMemory, + onValueChanged = { value -> + settingsViewModel.cacheCredentialsInMemory = value + } + ) +} + @Composable fun Terminal(settingsViewModel: SettingsViewModel) { var commitsLimit by remember { mutableStateOf(settingsViewModel.terminalPath) } @@ -272,7 +286,7 @@ private fun Branches(settingsViewModel: SettingsViewModel) { @Composable private fun Layout(settingsViewModel: SettingsViewModel) { - val swapUncommitedChanges by settingsViewModel.swapUncommitedChangesFlow.collectAsState() + val swapUncommitedChanges by settingsViewModel.swapUncommittedChangesFlow.collectAsState() SettingToggle( title = "Swap position for staged/unstaged views", diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt index 3fccea1..85ca481 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt @@ -129,7 +129,7 @@ private fun LogLoaded( repositoryState: RepositoryState ) { val scope = rememberCoroutineScope() - val hasUncommitedChanges = logStatus.hasUncommittedChanges + val hasUncommittedChanges = logStatus.hasUncommittedChanges val commitList = logStatus.plotCommitList val verticalScrollState by logViewModel.verticalListState.collectAsState() val horizontalScrollState by logViewModel.horizontalListState.collectAsState() @@ -219,7 +219,7 @@ private fun LogLoaded( MessagesList( scrollState = verticalScrollState, horizontalScrollState = horizontalScrollState, - hasUncommitedChanges = hasUncommitedChanges, + hasUncommittedChanges = hasUncommittedChanges, searchFilter = if (searchFilterValue is LogSearch.SearchResults) searchFilterValue.commits else null, selectedCommit = selectedCommit, logStatus = logStatus, @@ -427,7 +427,7 @@ fun SearchFilter( @Composable fun MessagesList( scrollState: LazyListState, - hasUncommitedChanges: Boolean, + hasUncommittedChanges: Boolean, searchFilter: List?, selectedCommit: RevCommit?, logStatus: LogStatus.Loaded, @@ -447,7 +447,7 @@ fun MessagesList( modifier = Modifier.fillMaxSize(), ) { if ( - hasUncommitedChanges || + hasUncommittedChanges || repositoryState.isMerging || repositoryState.isRebasing || repositoryState.isCherryPicking diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/LogViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/LogViewModel.kt index e8a9383..d2c8a3c 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/LogViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/LogViewModel.kt @@ -285,7 +285,7 @@ class LogViewModel @Inject constructor( if (previousLogStatusValue is LogStatus.Loaded) { val newLogStatusValue = LogStatus.Loaded( - hasUncommitedChanges = hasUncommitedChanges, + hasUncommittedChanges = hasUncommitedChanges, plotCommitList = previousLogStatusValue.plotCommitList, currentBranch = currentBranch, statusSummary = statsSummary, @@ -447,7 +447,7 @@ class LogViewModel @Inject constructor( sealed class LogStatus { object Loading : LogStatus() class Loaded( - val hasUncommitedChanges: Boolean, + val hasUncommittedChanges: Boolean, val plotCommitList: GraphCommitList, val currentBranch: Ref?, val statusSummary: StatusSummary, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt index 37f3b98..7d77a2e 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt @@ -26,7 +26,8 @@ class SettingsViewModel @Inject constructor( val pullRebaseFlow = appSettings.pullRebaseFlow val pushWithLeaseFlow = appSettings.pushWithLeaseFlow val commitsLimitEnabledFlow = appSettings.commitsLimitEnabledFlow - val swapUncommitedChangesFlow = appSettings.swapUncommitedChangesFlow + val swapUncommittedChangesFlow = appSettings.swapUncommittedChangesFlow + val cacheCredentialsInMemoryFlow = appSettings.cacheCredentialsInMemoryFlow val terminalPathFlow = appSettings.terminalPathFlow var scaleUi: Float @@ -42,9 +43,9 @@ class SettingsViewModel @Inject constructor( } var swapUncommitedChanges: Boolean - get() = appSettings.swapUncommitedChanges + get() = appSettings.swapUncommittedChanges set(value) { - appSettings.swapUncommitedChanges = value + appSettings.swapUncommittedChanges = value } var ffMerge: Boolean @@ -53,6 +54,12 @@ class SettingsViewModel @Inject constructor( appSettings.ffMerge = value } + var cacheCredentialsInMemory: Boolean + get() = appSettings.cacheCredentialsInMemory + set(value) { + appSettings.cacheCredentialsInMemory = value + } + var pullRebase: Boolean get() = appSettings.pullRebase set(value) { diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt index b9829dd..8425de9 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt @@ -72,7 +72,7 @@ class StatusViewModel @Inject constructor( private val _searchFilterStaged = MutableStateFlow(TextFieldValue("")) val searchFilterStaged: StateFlow = _searchFilterStaged - val swapUncommitedChanges = appSettings.swapUncommitedChangesFlow + val swapUncommitedChanges = appSettings.swapUncommittedChangesFlow val rebaseInteractiveState = sharedRepositoryStateManager.rebaseInteractiveState private val _stageState = MutableStateFlow(StageState.Loading) diff --git a/src/main/resources/password.svg b/src/main/resources/password.svg new file mode 100644 index 0000000..6cb7fad --- /dev/null +++ b/src/main/resources/password.svg @@ -0,0 +1 @@ + \ No newline at end of file