parent
0c3ced89b4
commit
9264d2cb7a
@ -40,6 +40,7 @@ object AppIcons {
|
|||||||
const val NETWORK = "network.svg"
|
const val NETWORK = "network.svg"
|
||||||
const val OPEN = "open.svg"
|
const val OPEN = "open.svg"
|
||||||
const val PALETTE = "palette.svg"
|
const val PALETTE = "palette.svg"
|
||||||
|
const val PASSWORD = "password.svg"
|
||||||
const val PASTE = "paste.svg"
|
const val PASTE = "paste.svg"
|
||||||
const val PERSON = "person.svg"
|
const val PERSON = "person.svg"
|
||||||
const val REFRESH = "refresh.svg"
|
const val REFRESH = "refresh.svg"
|
||||||
|
@ -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<CredentialsType>()
|
||||||
|
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<CredentialsType.HttpCredentials>().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
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package com.jetpackduba.gitnuro.credentials
|
package com.jetpackduba.gitnuro.credentials
|
||||||
|
|
||||||
|
import com.jetpackduba.gitnuro.git.remote_operations.CredentialsCache
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider
|
import org.eclipse.jgit.transport.CredentialsProvider
|
||||||
import org.eclipse.jgit.transport.RemoteSession
|
import org.eclipse.jgit.transport.RemoteSession
|
||||||
import org.eclipse.jgit.transport.SshSessionFactory
|
import org.eclipse.jgit.transport.SshSessionFactory
|
||||||
@ -9,26 +10,33 @@ import javax.inject.Inject
|
|||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
|
|
||||||
class GSessionManager @Inject constructor(
|
class GSessionManager @Inject constructor(
|
||||||
private val sessionProvider: Provider<GRemoteSession>
|
private val mySessionFactory: MySessionFactory,
|
||||||
) {
|
) {
|
||||||
fun generateSshSessionFactory(): SshSessionFactory {
|
fun generateSshSessionFactory(): MySessionFactory {
|
||||||
return object : SshSessionFactory() {
|
return mySessionFactory
|
||||||
override fun getSession(
|
}
|
||||||
uri: URIish,
|
}
|
||||||
credentialsProvider: CredentialsProvider?,
|
|
||||||
fs: FS?,
|
class MySessionFactory @Inject constructor(
|
||||||
tms: Int
|
private val sessionProvider: Provider<GRemoteSession>
|
||||||
): RemoteSession {
|
) : SshSessionFactory(), CredentialsCache {
|
||||||
val remoteSession = sessionProvider.get()
|
override fun getSession(
|
||||||
remoteSession.setup(uri)
|
uri: URIish,
|
||||||
|
credentialsProvider: CredentialsProvider?,
|
||||||
return remoteSession
|
fs: FS?,
|
||||||
}
|
tms: Int
|
||||||
|
): RemoteSession {
|
||||||
override fun getType(): String {
|
val remoteSession = sessionProvider.get()
|
||||||
return "ssh" //TODO What should be the value of this?
|
remoteSession.setup(uri)
|
||||||
}
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,9 @@
|
|||||||
package com.jetpackduba.gitnuro.credentials
|
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.managers.IShellManager
|
import com.jetpackduba.gitnuro.managers.IShellManager
|
||||||
|
import com.jetpackduba.gitnuro.preferences.AppSettings
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
@ -18,8 +20,13 @@ private const val TIMEOUT_MIN = 1L
|
|||||||
class HttpCredentialsProvider @AssistedInject constructor(
|
class HttpCredentialsProvider @AssistedInject constructor(
|
||||||
private val credentialsStateManager: CredentialsStateManager,
|
private val credentialsStateManager: CredentialsStateManager,
|
||||||
private val shellManager: IShellManager,
|
private val shellManager: IShellManager,
|
||||||
|
private val appSettings: AppSettings,
|
||||||
|
private val credentialsCacheRepository: CredentialsCacheRepository,
|
||||||
@Assisted val git: Git?,
|
@Assisted val git: Git?,
|
||||||
) : CredentialsProvider() {
|
) : CredentialsProvider(), CredentialsCache {
|
||||||
|
|
||||||
|
private var credentialsCached: CredentialsType.HttpCredentials? = null
|
||||||
|
|
||||||
override fun isInteractive(): Boolean {
|
override fun isInteractive(): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -45,15 +52,32 @@ class HttpCredentialsProvider @AssistedInject constructor(
|
|||||||
val externalCredentialsHelper = getExternalCredentialsHelper(uri, git)
|
val externalCredentialsHelper = getExternalCredentialsHelper(uri, git)
|
||||||
|
|
||||||
if (externalCredentialsHelper == null) {
|
if (externalCredentialsHelper == null) {
|
||||||
val credentials = askForCredentials()
|
val cachedCredentials = credentialsCacheRepository.getCachedHttpCredentials(uri.toString())
|
||||||
|
|
||||||
if (credentials is CredentialsAccepted.HttpCredentialsAccepted) {
|
if (cachedCredentials == null) {
|
||||||
userItem.value = credentials.user
|
val credentials = askForCredentials()
|
||||||
passwordItem.value = credentials.password.toCharArray()
|
|
||||||
|
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
|
return true
|
||||||
} else if (credentials is CredentialsState.CredentialsDenied) {
|
|
||||||
throw CancellationException("Credentials denied")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@ -153,26 +177,26 @@ class HttpCredentialsProvider @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
bufferedReader.use {
|
bufferedReader.use {
|
||||||
var line: String
|
var line: String?
|
||||||
while (bufferedReader.readLine().also {
|
while (bufferedReader.readLine().also { line = it } != null && !(usernameSet && passwordSet)) {
|
||||||
line = checkNotNull(it) { "Cancelled authentication" }
|
val safeLine = line ?: continue
|
||||||
} != null && !(usernameSet && passwordSet)) {
|
|
||||||
if (line.startsWith("username=")) {
|
if (safeLine.startsWith("username=")) {
|
||||||
val split = line.split("=")
|
val split = safeLine.split("=")
|
||||||
val userName = split.getOrNull(1) ?: return ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED
|
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) {
|
if (userNameItem is CredentialItem.Username) {
|
||||||
userNameItem.value = userName
|
userNameItem.value = userName
|
||||||
usernameSet = true
|
usernameSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (line.startsWith("password=")) {
|
} else if (safeLine.startsWith("password=")) {
|
||||||
val split = line.split("=")
|
val split = safeLine.split("=")
|
||||||
val password = split.getOrNull(1) ?: return ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED
|
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) {
|
if (passwordItem is CredentialItem.Password) {
|
||||||
passwordItem.value = password.toCharArray()
|
passwordItem.value = password.toCharArray()
|
||||||
@ -182,9 +206,9 @@ class HttpCredentialsProvider @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (usernameSet && passwordSet)
|
return if (usernameSet && passwordSet) {
|
||||||
ExternalCredentialsRequestResult.SUCCESS
|
ExternalCredentialsRequestResult.SUCCESS
|
||||||
else
|
} else
|
||||||
ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED
|
ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +230,7 @@ class HttpCredentialsProvider @AssistedInject constructor(
|
|||||||
|
|
||||||
val genericCredentialHelper = config.getString("credential", null, "helper")
|
val genericCredentialHelper = config.getString("credential", null, "helper")
|
||||||
val uriSpecificCredentialHelper = config.getString("credential", hostWithProtocol, "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") {
|
if (credentialHelperPath == "cache" || credentialHelperPath == "store") {
|
||||||
throw NotSupportedHelper("Invalid credentials helper: \"$credentialHelperPath\" is not yet supported")
|
throw NotSupportedHelper("Invalid credentials helper: \"$credentialHelperPath\" is not yet supported")
|
||||||
@ -225,6 +249,12 @@ class HttpCredentialsProvider @AssistedInject constructor(
|
|||||||
|
|
||||||
return ExternalCredentialsHelper(credentialHelperPath, useHttpPath)
|
return ExternalCredentialsHelper(credentialHelperPath, useHttpPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun cacheCredentialsIfNeeded() {
|
||||||
|
credentialsCached?.let {
|
||||||
|
credentialsCacheRepository.cacheHttpCredentials(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ExternalCredentialsHelper(
|
data class ExternalCredentialsHelper(
|
||||||
|
@ -2,6 +2,7 @@ package com.jetpackduba.gitnuro.di
|
|||||||
|
|
||||||
import com.jetpackduba.gitnuro.App
|
import com.jetpackduba.gitnuro.App
|
||||||
import com.jetpackduba.gitnuro.AppEnvInfo
|
import com.jetpackduba.gitnuro.AppEnvInfo
|
||||||
|
import com.jetpackduba.gitnuro.credentials.CredentialsCacheRepository
|
||||||
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager
|
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager
|
||||||
import com.jetpackduba.gitnuro.di.modules.AppModule
|
import com.jetpackduba.gitnuro.di.modules.AppModule
|
||||||
import com.jetpackduba.gitnuro.di.modules.NetworkModule
|
import com.jetpackduba.gitnuro.di.modules.NetworkModule
|
||||||
@ -44,4 +45,6 @@ interface AppComponent {
|
|||||||
fun tempFilesManager(): TempFilesManager
|
fun tempFilesManager(): TempFilesManager
|
||||||
|
|
||||||
fun updatesRepository(): UpdatesRepository
|
fun updatesRepository(): UpdatesRepository
|
||||||
|
|
||||||
|
fun credentialsCacheRepository(): CredentialsCacheRepository
|
||||||
}
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.jetpackduba.gitnuro.extensions
|
||||||
|
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
|
||||||
|
suspend fun <T> Mutex.lockUse(block: () -> T): T {
|
||||||
|
this.lock()
|
||||||
|
|
||||||
|
try {
|
||||||
|
return block()
|
||||||
|
} finally {
|
||||||
|
this.unlock()
|
||||||
|
}
|
||||||
|
}
|
@ -26,46 +26,47 @@ class CloneRepositoryUseCase @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
ensureActive()
|
ensureActive()
|
||||||
trySend(CloneState.Cloning("Starting...", progress, lastTotalWork))
|
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()
|
override fun beginTask(title: String?, totalWork: Int) {
|
||||||
.setDirectory(directory)
|
println("ProgressMonitor Begin task with title: $title")
|
||||||
.setURI(url)
|
lastTitle = title.orEmpty()
|
||||||
.setProgressMonitor(
|
lastTotalWork = totalWork
|
||||||
object : ProgressMonitor {
|
progress = 0
|
||||||
override fun start(totalTasks: Int) {
|
trySend(CloneState.Cloning(lastTitle, progress, lastTotalWork))
|
||||||
printDebug(TAG, "ProgressMonitor Start with total tasks of: $totalTasks")
|
}
|
||||||
|
|
||||||
|
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) {
|
.setTransportConfigCallback { handleTransport(it) }
|
||||||
println("ProgressMonitor Begin task with title: $title")
|
.setCloneSubmodules(cloneSubmodules)
|
||||||
lastTitle = title.orEmpty()
|
.call()
|
||||||
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()
|
|
||||||
|
|
||||||
ensureActive()
|
ensureActive()
|
||||||
trySend(CloneState.Completed(directory))
|
trySend(CloneState.Completed(directory))
|
||||||
|
@ -25,35 +25,33 @@ class DeleteRemoteBranchUseCase @Inject constructor(
|
|||||||
.setSource(null)
|
.setSource(null)
|
||||||
.setDestination(branchName)
|
.setDestination(branchName)
|
||||||
|
|
||||||
val pushResults = git.push()
|
handleTransportUseCase(git) {
|
||||||
.setTransportConfigCallback {
|
val pushResults = git.push()
|
||||||
handleTransportUseCase(it, git)
|
.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)
|
deleteBranchUseCase(git, ref)
|
||||||
// git
|
|
||||||
// .branchDelete()
|
|
||||||
// .setBranchNames(ref.name)
|
|
||||||
// .call()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -23,26 +23,28 @@ class FetchAllBranchesUseCase @Inject constructor(
|
|||||||
val errors = mutableListOf<Pair<RemoteConfig, Exception>>()
|
val errors = mutableListOf<Pair<RemoteConfig, Exception>>()
|
||||||
for (remote in remotes) {
|
for (remote in remotes) {
|
||||||
try {
|
try {
|
||||||
git.fetch()
|
handleTransportUseCase(git) {
|
||||||
.setRemote(remote.name)
|
git.fetch()
|
||||||
.setRefSpecs(remote.fetchRefSpecs)
|
.setRemote(remote.name)
|
||||||
.setRemoveDeletedRefs(true)
|
.setRefSpecs(remote.fetchRefSpecs)
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it, git) }
|
.setRemoveDeletedRefs(true)
|
||||||
.setCredentialsProvider(CredentialsProvider.getDefault())
|
.setTransportConfigCallback { handleTransport(it) }
|
||||||
.setProgressMonitor(object : ProgressMonitor {
|
.setCredentialsProvider(CredentialsProvider.getDefault())
|
||||||
override fun start(totalTasks: Int) {}
|
.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) {}
|
override fun showDuration(enabled: Boolean) {}
|
||||||
})
|
})
|
||||||
.call()
|
.call()
|
||||||
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
printError(TAG, "Fetch failed for remote ${remote.name} with error ${ex.message}", ex)
|
printError(TAG, "Fetch failed for remote ${remote.name} with error ${ex.message}", ex)
|
||||||
|
|
||||||
|
@ -12,11 +12,39 @@ class HandleTransportUseCase @Inject constructor(
|
|||||||
private val sessionManager: GSessionManager,
|
private val sessionManager: GSessionManager,
|
||||||
private val httpCredentialsProvider: HttpCredentialsFactory,
|
private val httpCredentialsProvider: HttpCredentialsFactory,
|
||||||
) {
|
) {
|
||||||
operator fun invoke(transport: Transport?, git: Git?) {
|
suspend operator fun invoke(git: Git?, block: suspend CredentialsHandler.() -> Unit) {
|
||||||
if (transport is SshTransport) {
|
var cache: CredentialsCache? = null
|
||||||
transport.sshSessionFactory = sessionManager.generateSshSessionFactory()
|
|
||||||
} else if (transport is HttpTransport) {
|
val credentialsHandler = object: CredentialsHandler {
|
||||||
transport.credentialsProvider = httpCredentialsProvider.create(git)
|
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?)
|
||||||
|
}
|
@ -4,6 +4,7 @@ import com.jetpackduba.gitnuro.preferences.AppSettings
|
|||||||
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.PullResult
|
||||||
import org.eclipse.jgit.api.RebaseResult
|
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
|
||||||
@ -19,25 +20,27 @@ class PullBranchUseCase @Inject constructor(
|
|||||||
PullType.DEFAULT -> appSettings.pullRebase
|
PullType.DEFAULT -> appSettings.pullRebase
|
||||||
}
|
}
|
||||||
|
|
||||||
val pullResult = git
|
handleTransportUseCase(git) {
|
||||||
.pull()
|
val pullResult = git
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it, git) }
|
.pull()
|
||||||
.setRebase(pullWithRebase)
|
.setTransportConfigCallback {this.handleTransport(it) }
|
||||||
.setCredentialsProvider(CredentialsProvider.getDefault())
|
.setRebase(pullWithRebase)
|
||||||
.call()
|
.setCredentialsProvider(CredentialsProvider.getDefault())
|
||||||
|
.call()
|
||||||
|
|
||||||
if (!pullResult.isSuccessful) {
|
if (!pullResult.isSuccessful) {
|
||||||
var message = "Pull failed"
|
var message = "Pull failed"
|
||||||
|
|
||||||
if (pullWithRebase) {
|
if (pullWithRebase) {
|
||||||
message = when (pullResult.rebaseResult.status) {
|
message = when (pullResult.rebaseResult.status) {
|
||||||
RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommited changes"
|
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"
|
RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue"
|
||||||
else -> message
|
else -> message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
throw Exception(message)
|
throw Exception(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,27 +14,30 @@ class PullFromSpecificBranchUseCase @Inject constructor(
|
|||||||
private val handleTransportUseCase: HandleTransportUseCase,
|
private val handleTransportUseCase: HandleTransportUseCase,
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(git: Git, rebase: Boolean, remoteBranch: Ref) = withContext(Dispatchers.IO) {
|
suspend operator fun invoke(git: Git, rebase: Boolean, remoteBranch: Ref) = withContext(Dispatchers.IO) {
|
||||||
val pullResult = git
|
handleTransportUseCase(git) {
|
||||||
.pull()
|
val pullResult = git
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it, git) }
|
.pull()
|
||||||
.setRemote(remoteBranch.remoteName)
|
.setTransportConfigCallback { handleTransport(it) }
|
||||||
.setRemoteBranchName(remoteBranch.simpleName)
|
.setRemote(remoteBranch.remoteName)
|
||||||
.setRebase(rebase)
|
.setRemoteBranchName(remoteBranch.simpleName)
|
||||||
.setCredentialsProvider(CredentialsProvider.getDefault())
|
.setRebase(rebase)
|
||||||
.call()
|
.setCredentialsProvider(CredentialsProvider.getDefault())
|
||||||
|
.call()
|
||||||
|
|
||||||
if (!pullResult.isSuccessful) {
|
if (!pullResult.isSuccessful) {
|
||||||
var message = "Pull failed" // TODO Remove messages from here and pass the result to a custom exception type
|
var message =
|
||||||
|
"Pull failed" // TODO Remove messages from here and pass the result to a custom exception type
|
||||||
|
|
||||||
if (rebase) {
|
if (rebase) {
|
||||||
message = when (pullResult.rebaseResult.status) {
|
message = when (pullResult.rebaseResult.status) {
|
||||||
RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommited changes"
|
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"
|
RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue"
|
||||||
else -> message
|
else -> message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
throw Exception(message)
|
throw Exception(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
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.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
|
||||||
import com.jetpackduba.gitnuro.preferences.AppSettings
|
import com.jetpackduba.gitnuro.preferences.AppSettings
|
||||||
@ -27,7 +28,18 @@ class PushBranchUseCase @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
currentBranch
|
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
|
val pushResult = git
|
||||||
.push()
|
.push()
|
||||||
.setRefSpecs(RefSpec(refSpecStr))
|
.setRefSpecs(RefSpec(refSpecStr))
|
||||||
@ -66,7 +78,7 @@ class PushBranchUseCase @Inject constructor(
|
|||||||
this
|
this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it, git) }
|
.setTransportConfigCallback { handleTransport(it) }
|
||||||
.setProgressMonitor(object : ProgressMonitor {
|
.setProgressMonitor(object : ProgressMonitor {
|
||||||
override fun start(totalTasks: Int) {}
|
override fun start(totalTasks: Int) {}
|
||||||
override fun beginTask(title: String?, totalWork: Int) {}
|
override fun beginTask(title: String?, totalWork: Int) {}
|
||||||
@ -105,5 +117,4 @@ class PushBranchUseCase @Inject constructor(
|
|||||||
throw Exception(error.toString())
|
throw Exception(error.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -18,30 +18,32 @@ class PushToSpecificBranchUseCase @Inject constructor(
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val currentBranchRefSpec = git.repository.fullBranch
|
val currentBranchRefSpec = git.repository.fullBranch
|
||||||
|
|
||||||
val pushResult = git
|
handleTransportUseCase(git) {
|
||||||
.push()
|
val pushResult = git
|
||||||
.setRefSpecs(RefSpec("$currentBranchRefSpec:${remoteBranch.simpleName}"))
|
.push()
|
||||||
.setRemote(remoteBranch.remoteName)
|
.setRefSpecs(RefSpec("$currentBranchRefSpec:${remoteBranch.simpleName}"))
|
||||||
.setForce(force)
|
.setRemote(remoteBranch.remoteName)
|
||||||
.apply {
|
.setForce(force)
|
||||||
if (pushTags)
|
.apply {
|
||||||
setPushTags()
|
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,22 +13,24 @@ class AddSubmoduleUseCase @Inject constructor(
|
|||||||
private val handleTransportUseCase: HandleTransportUseCase,
|
private val handleTransportUseCase: HandleTransportUseCase,
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(git: Git, name: String, path: String, uri: String): Unit = withContext(Dispatchers.IO) {
|
suspend operator fun invoke(git: Git, name: String, path: String, uri: String): Unit = withContext(Dispatchers.IO) {
|
||||||
git.submoduleAdd()
|
handleTransportUseCase(git) {
|
||||||
.setName(name)
|
git.submoduleAdd()
|
||||||
.setPath(path)
|
.setName(name)
|
||||||
.setURI(uri)
|
.setPath(path)
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it, git) }
|
.setURI(uri)
|
||||||
.setCredentialsProvider(CredentialsProvider.getDefault())
|
.setTransportConfigCallback { handleTransport(it) }
|
||||||
.setProgressMonitor(object : ProgressMonitor {
|
.setCredentialsProvider(CredentialsProvider.getDefault())
|
||||||
override fun start(totalTasks: Int) {}
|
.setProgressMonitor(object : ProgressMonitor {
|
||||||
override fun beginTask(title: String?, totalWork: Int) {}
|
override fun start(totalTasks: Int) {}
|
||||||
override fun update(completed: Int) {}
|
override fun beginTask(title: String?, totalWork: Int) {}
|
||||||
override fun endTask() {}
|
override fun update(completed: Int) {}
|
||||||
override fun showDuration(enabled: Boolean) {}
|
override fun endTask() {}
|
||||||
|
override fun showDuration(enabled: Boolean) {}
|
||||||
|
|
||||||
override fun isCancelled() = !isActive
|
override fun isCancelled() = !isActive
|
||||||
|
|
||||||
})
|
})
|
||||||
.call()
|
.call()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,32 +16,34 @@ class UpdateSubmoduleUseCase @Inject constructor(
|
|||||||
private val handleTransportUseCase: HandleTransportUseCase,
|
private val handleTransportUseCase: HandleTransportUseCase,
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(git: Git, path: String) = withContext(Dispatchers.IO) {
|
suspend operator fun invoke(git: Git, path: String) = withContext(Dispatchers.IO) {
|
||||||
git.submoduleUpdate()
|
handleTransportUseCase(git) {
|
||||||
.addPath(path)
|
git.submoduleUpdate()
|
||||||
.setCallback(
|
.addPath(path)
|
||||||
object : CloneCommand.Callback {
|
.setCallback(
|
||||||
override fun initializedSubmodules(submodules: MutableCollection<String>?) {
|
object : CloneCommand.Callback {
|
||||||
|
override fun initializedSubmodules(submodules: MutableCollection<String>?) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cloningSubmodule(path: String?) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkingOut(commit: AnyObjectId?, path: String?) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
override fun cloningSubmodule(path: String?) {
|
.setTransportConfigCallback { handleTransport(it) }
|
||||||
|
.setProgressMonitor(object : ProgressMonitor {
|
||||||
}
|
override fun start(totalTasks: Int) {}
|
||||||
|
override fun beginTask(title: String?, totalWork: Int) {}
|
||||||
override fun checkingOut(commit: AnyObjectId?, path: String?) {
|
override fun update(completed: Int) {}
|
||||||
|
override fun endTask() {}
|
||||||
}
|
override fun isCancelled(): Boolean = !isActive
|
||||||
}
|
override fun showDuration(enabled: Boolean) {}
|
||||||
)
|
})
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it, git) }
|
.call()
|
||||||
.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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -28,8 +28,9 @@ private const val PREF_CUSTOM_THEME = "customTheme"
|
|||||||
private const val PREF_UI_SCALE = "ui_scale"
|
private const val PREF_UI_SCALE = "ui_scale"
|
||||||
private const val PREF_DIFF_TYPE = "diffType"
|
private const val PREF_DIFF_TYPE = "diffType"
|
||||||
private const val PREF_DIFF_FULL_FILE = "diffFullFile"
|
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_TERMINAL_PATH = "terminalPath"
|
||||||
|
private const val PREF_CACHE_CREDENTIALS_IN_MEMORY = "credentialsInMemory"
|
||||||
|
|
||||||
|
|
||||||
private const val PREF_GIT_FF_MERGE = "gitFFMerge"
|
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 = 1000
|
||||||
private const val DEFAULT_COMMITS_LIMIT_ENABLED = true
|
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
|
const val DEFAULT_UI_SCALE = -1f
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@ -51,8 +53,11 @@ class AppSettings @Inject constructor() {
|
|||||||
private val _commitsLimitEnabledFlow = MutableStateFlow(commitsLimitEnabled)
|
private val _commitsLimitEnabledFlow = MutableStateFlow(commitsLimitEnabled)
|
||||||
val commitsLimitEnabledFlow = _commitsLimitEnabledFlow.asStateFlow()
|
val commitsLimitEnabledFlow = _commitsLimitEnabledFlow.asStateFlow()
|
||||||
|
|
||||||
private val _swapUncommitedChangesFlow = MutableStateFlow(swapUncommitedChanges)
|
private val _swapUncommittedChangesFlow = MutableStateFlow(swapUncommittedChanges)
|
||||||
val swapUncommitedChangesFlow = _swapUncommitedChangesFlow.asStateFlow()
|
val swapUncommittedChangesFlow = _swapUncommittedChangesFlow.asStateFlow()
|
||||||
|
|
||||||
|
private val _cacheCredentialsInMemoryFlow = MutableStateFlow(cacheCredentialsInMemory)
|
||||||
|
val cacheCredentialsInMemoryFlow = _cacheCredentialsInMemoryFlow.asStateFlow()
|
||||||
|
|
||||||
private val _ffMergeFlow = MutableStateFlow(ffMerge)
|
private val _ffMergeFlow = MutableStateFlow(ffMerge)
|
||||||
val ffMergeFlow = _ffMergeFlow.asStateFlow()
|
val ffMergeFlow = _ffMergeFlow.asStateFlow()
|
||||||
@ -123,13 +128,22 @@ class AppSettings @Inject constructor() {
|
|||||||
_commitsLimitEnabledFlow.value = value
|
_commitsLimitEnabledFlow.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
var swapUncommitedChanges: Boolean
|
var swapUncommittedChanges: Boolean
|
||||||
get() {
|
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) {
|
set(value) {
|
||||||
preferences.putBoolean(PREF_SWAP_UNCOMMITED_CHANGES, value)
|
preferences.putBoolean(PREF_SWAP_UNCOMMITTED_CHANGES, value)
|
||||||
_swapUncommitedChangesFlow.value = 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
|
var scaleUi: Float
|
||||||
|
@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.Switch
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -17,7 +16,6 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
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.extensions.handMouseClickable
|
import com.jetpackduba.gitnuro.extensions.handMouseClickable
|
||||||
import com.jetpackduba.gitnuro.extensions.handOnHover
|
|
||||||
import com.jetpackduba.gitnuro.managers.Error
|
import com.jetpackduba.gitnuro.managers.Error
|
||||||
import com.jetpackduba.gitnuro.preferences.DEFAULT_UI_SCALE
|
import com.jetpackduba.gitnuro.preferences.DEFAULT_UI_SCALE
|
||||||
import com.jetpackduba.gitnuro.theme.*
|
import com.jetpackduba.gitnuro.theme.*
|
||||||
@ -48,6 +46,7 @@ val settings = listOf(
|
|||||||
|
|
||||||
SettingsEntry.Section("Network"),
|
SettingsEntry.Section("Network"),
|
||||||
SettingsEntry.Entry(AppIcons.NETWORK, "Proxy") { },
|
SettingsEntry.Entry(AppIcons.NETWORK, "Proxy") { },
|
||||||
|
SettingsEntry.Entry(AppIcons.PASSWORD, "Authentication") { Authentication(it) },
|
||||||
|
|
||||||
SettingsEntry.Section("Tools"),
|
SettingsEntry.Section("Tools"),
|
||||||
SettingsEntry.Entry(AppIcons.TERMINAL, "Terminal") { Terminal(it) },
|
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
|
@Composable
|
||||||
fun Terminal(settingsViewModel: SettingsViewModel) {
|
fun Terminal(settingsViewModel: SettingsViewModel) {
|
||||||
var commitsLimit by remember { mutableStateOf(settingsViewModel.terminalPath) }
|
var commitsLimit by remember { mutableStateOf(settingsViewModel.terminalPath) }
|
||||||
@ -272,7 +286,7 @@ private fun Branches(settingsViewModel: SettingsViewModel) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Layout(settingsViewModel: SettingsViewModel) {
|
private fun Layout(settingsViewModel: SettingsViewModel) {
|
||||||
val swapUncommitedChanges by settingsViewModel.swapUncommitedChangesFlow.collectAsState()
|
val swapUncommitedChanges by settingsViewModel.swapUncommittedChangesFlow.collectAsState()
|
||||||
|
|
||||||
SettingToggle(
|
SettingToggle(
|
||||||
title = "Swap position for staged/unstaged views",
|
title = "Swap position for staged/unstaged views",
|
||||||
|
@ -129,7 +129,7 @@ private fun LogLoaded(
|
|||||||
repositoryState: RepositoryState
|
repositoryState: RepositoryState
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val hasUncommitedChanges = logStatus.hasUncommittedChanges
|
val hasUncommittedChanges = logStatus.hasUncommittedChanges
|
||||||
val commitList = logStatus.plotCommitList
|
val commitList = logStatus.plotCommitList
|
||||||
val verticalScrollState by logViewModel.verticalListState.collectAsState()
|
val verticalScrollState by logViewModel.verticalListState.collectAsState()
|
||||||
val horizontalScrollState by logViewModel.horizontalListState.collectAsState()
|
val horizontalScrollState by logViewModel.horizontalListState.collectAsState()
|
||||||
@ -219,7 +219,7 @@ private fun LogLoaded(
|
|||||||
MessagesList(
|
MessagesList(
|
||||||
scrollState = verticalScrollState,
|
scrollState = verticalScrollState,
|
||||||
horizontalScrollState = horizontalScrollState,
|
horizontalScrollState = horizontalScrollState,
|
||||||
hasUncommitedChanges = hasUncommitedChanges,
|
hasUncommittedChanges = hasUncommittedChanges,
|
||||||
searchFilter = if (searchFilterValue is LogSearch.SearchResults) searchFilterValue.commits else null,
|
searchFilter = if (searchFilterValue is LogSearch.SearchResults) searchFilterValue.commits else null,
|
||||||
selectedCommit = selectedCommit,
|
selectedCommit = selectedCommit,
|
||||||
logStatus = logStatus,
|
logStatus = logStatus,
|
||||||
@ -427,7 +427,7 @@ fun SearchFilter(
|
|||||||
@Composable
|
@Composable
|
||||||
fun MessagesList(
|
fun MessagesList(
|
||||||
scrollState: LazyListState,
|
scrollState: LazyListState,
|
||||||
hasUncommitedChanges: Boolean,
|
hasUncommittedChanges: Boolean,
|
||||||
searchFilter: List<GraphNode>?,
|
searchFilter: List<GraphNode>?,
|
||||||
selectedCommit: RevCommit?,
|
selectedCommit: RevCommit?,
|
||||||
logStatus: LogStatus.Loaded,
|
logStatus: LogStatus.Loaded,
|
||||||
@ -447,7 +447,7 @@ fun MessagesList(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
hasUncommitedChanges ||
|
hasUncommittedChanges ||
|
||||||
repositoryState.isMerging ||
|
repositoryState.isMerging ||
|
||||||
repositoryState.isRebasing ||
|
repositoryState.isRebasing ||
|
||||||
repositoryState.isCherryPicking
|
repositoryState.isCherryPicking
|
||||||
|
@ -285,7 +285,7 @@ class LogViewModel @Inject constructor(
|
|||||||
|
|
||||||
if (previousLogStatusValue is LogStatus.Loaded) {
|
if (previousLogStatusValue is LogStatus.Loaded) {
|
||||||
val newLogStatusValue = LogStatus.Loaded(
|
val newLogStatusValue = LogStatus.Loaded(
|
||||||
hasUncommitedChanges = hasUncommitedChanges,
|
hasUncommittedChanges = hasUncommitedChanges,
|
||||||
plotCommitList = previousLogStatusValue.plotCommitList,
|
plotCommitList = previousLogStatusValue.plotCommitList,
|
||||||
currentBranch = currentBranch,
|
currentBranch = currentBranch,
|
||||||
statusSummary = statsSummary,
|
statusSummary = statsSummary,
|
||||||
@ -447,7 +447,7 @@ class LogViewModel @Inject constructor(
|
|||||||
sealed class LogStatus {
|
sealed class LogStatus {
|
||||||
object Loading : LogStatus()
|
object Loading : LogStatus()
|
||||||
class Loaded(
|
class Loaded(
|
||||||
val hasUncommitedChanges: Boolean,
|
val hasUncommittedChanges: Boolean,
|
||||||
val plotCommitList: GraphCommitList,
|
val plotCommitList: GraphCommitList,
|
||||||
val currentBranch: Ref?,
|
val currentBranch: Ref?,
|
||||||
val statusSummary: StatusSummary,
|
val statusSummary: StatusSummary,
|
||||||
|
@ -26,7 +26,8 @@ class SettingsViewModel @Inject constructor(
|
|||||||
val pullRebaseFlow = appSettings.pullRebaseFlow
|
val pullRebaseFlow = appSettings.pullRebaseFlow
|
||||||
val pushWithLeaseFlow = appSettings.pushWithLeaseFlow
|
val pushWithLeaseFlow = appSettings.pushWithLeaseFlow
|
||||||
val commitsLimitEnabledFlow = appSettings.commitsLimitEnabledFlow
|
val commitsLimitEnabledFlow = appSettings.commitsLimitEnabledFlow
|
||||||
val swapUncommitedChangesFlow = appSettings.swapUncommitedChangesFlow
|
val swapUncommittedChangesFlow = appSettings.swapUncommittedChangesFlow
|
||||||
|
val cacheCredentialsInMemoryFlow = appSettings.cacheCredentialsInMemoryFlow
|
||||||
val terminalPathFlow = appSettings.terminalPathFlow
|
val terminalPathFlow = appSettings.terminalPathFlow
|
||||||
|
|
||||||
var scaleUi: Float
|
var scaleUi: Float
|
||||||
@ -42,9 +43,9 @@ class SettingsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var swapUncommitedChanges: Boolean
|
var swapUncommitedChanges: Boolean
|
||||||
get() = appSettings.swapUncommitedChanges
|
get() = appSettings.swapUncommittedChanges
|
||||||
set(value) {
|
set(value) {
|
||||||
appSettings.swapUncommitedChanges = value
|
appSettings.swapUncommittedChanges = value
|
||||||
}
|
}
|
||||||
|
|
||||||
var ffMerge: Boolean
|
var ffMerge: Boolean
|
||||||
@ -53,6 +54,12 @@ class SettingsViewModel @Inject constructor(
|
|||||||
appSettings.ffMerge = value
|
appSettings.ffMerge = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cacheCredentialsInMemory: Boolean
|
||||||
|
get() = appSettings.cacheCredentialsInMemory
|
||||||
|
set(value) {
|
||||||
|
appSettings.cacheCredentialsInMemory = value
|
||||||
|
}
|
||||||
|
|
||||||
var pullRebase: Boolean
|
var pullRebase: Boolean
|
||||||
get() = appSettings.pullRebase
|
get() = appSettings.pullRebase
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@ -72,7 +72,7 @@ class StatusViewModel @Inject constructor(
|
|||||||
private val _searchFilterStaged = MutableStateFlow(TextFieldValue(""))
|
private val _searchFilterStaged = MutableStateFlow(TextFieldValue(""))
|
||||||
val searchFilterStaged: StateFlow<TextFieldValue> = _searchFilterStaged
|
val searchFilterStaged: StateFlow<TextFieldValue> = _searchFilterStaged
|
||||||
|
|
||||||
val swapUncommitedChanges = appSettings.swapUncommitedChangesFlow
|
val swapUncommitedChanges = appSettings.swapUncommittedChangesFlow
|
||||||
val rebaseInteractiveState = sharedRepositoryStateManager.rebaseInteractiveState
|
val rebaseInteractiveState = sharedRepositoryStateManager.rebaseInteractiveState
|
||||||
|
|
||||||
private val _stageState = MutableStateFlow<StageState>(StageState.Loading)
|
private val _stageState = MutableStateFlow<StageState>(StageState.Loading)
|
||||||
|
1
src/main/resources/password.svg
Normal file
1
src/main/resources/password.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M80-200v-61h800v61H80Zm38-254-40-22 40-68H40v-45h78l-40-68 40-22 38 67 38-67 40 22-40 68h78v45h-78l40 68-40 22-38-67-38 67Zm324 0-40-24 40-68h-78v-45h78l-40-68 40-22 38 67 38-67 40 22-40 68h78v45h-78l40 68-40 24-38-67-38 67Zm324 0-40-24 40-68h-78v-45h78l-40-68 40-22 38 67 38-67 40 22-40 68h78v45h-78l40 68-40 24-38-67-38 67Z"/></svg>
|
After Width: | Height: | Size: 431 B |
Loading…
Reference in New Issue
Block a user