parent
0c3ced89b4
commit
9264d2cb7a
@ -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"
|
||||
|
@ -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
|
||||
|
||||
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<GRemoteSession>
|
||||
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)
|
||||
|
||||
return remoteSession
|
||||
}
|
||||
|
||||
override fun getType(): String {
|
||||
return "ssh" //TODO What should be the value of this?
|
||||
}
|
||||
|
||||
}
|
||||
fun generateSshSessionFactory(): MySessionFactory {
|
||||
return mySessionFactory
|
||||
}
|
||||
}
|
||||
|
||||
class MySessionFactory @Inject constructor(
|
||||
private val sessionProvider: Provider<GRemoteSession>
|
||||
) : SshSessionFactory(), CredentialsCache {
|
||||
override fun getSession(
|
||||
uri: URIish,
|
||||
credentialsProvider: CredentialsProvider?,
|
||||
fs: FS?,
|
||||
tms: Int
|
||||
): RemoteSession {
|
||||
val remoteSession = sessionProvider.get()
|
||||
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
|
||||
|
||||
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(
|
||||
|
@ -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
|
||||
|
||||
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 {
|
||||
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))
|
||||
|
@ -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()
|
||||
|
||||
}
|
||||
}
|
@ -23,26 +23,28 @@ class FetchAllBranchesUseCase @Inject constructor(
|
||||
val errors = mutableListOf<Pair<RemoteConfig, Exception>>()
|
||||
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)
|
||||
|
||||
|
@ -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?)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String>?) {
|
||||
handleTransportUseCase(git) {
|
||||
git.submoduleUpdate()
|
||||
.addPath(path)
|
||||
.setCallback(
|
||||
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?) {
|
||||
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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<GraphNode>?,
|
||||
selectedCommit: RevCommit?,
|
||||
logStatus: LogStatus.Loaded,
|
||||
@ -447,7 +447,7 @@ fun MessagesList(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
if (
|
||||
hasUncommitedChanges ||
|
||||
hasUncommittedChanges ||
|
||||
repositoryState.isMerging ||
|
||||
repositoryState.isRebasing ||
|
||||
repositoryState.isCherryPicking
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -72,7 +72,7 @@ class StatusViewModel @Inject constructor(
|
||||
private val _searchFilterStaged = MutableStateFlow(TextFieldValue(""))
|
||||
val searchFilterStaged: StateFlow<TextFieldValue> = _searchFilterStaged
|
||||
|
||||
val swapUncommitedChanges = appSettings.swapUncommitedChangesFlow
|
||||
val swapUncommitedChanges = appSettings.swapUncommittedChangesFlow
|
||||
val rebaseInteractiveState = sharedRepositoryStateManager.rebaseInteractiveState
|
||||
|
||||
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