diff --git a/build.gradle.kts b/build.gradle.kts index e7d0dfa..c496664 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,12 +24,15 @@ repositories { } dependencies { + val jgit = "6.4.0.202211300538-r" + implementation(compose.desktop.currentOs) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.desktop.components.splitPane) implementation(compose("org.jetbrains.compose.ui:ui-util")) implementation(compose("org.jetbrains.compose.components:components-animatedimage")) - implementation("org.eclipse.jgit:org.eclipse.jgit:6.4.0.202211300538-r") + implementation("org.eclipse.jgit:org.eclipse.jgit:$jgit") + implementation("org.eclipse.jgit:org.eclipse.jgit.gpg.bc:$jgit") implementation("org.apache.sshd:sshd-core:2.9.0") implementation("com.google.dagger:dagger:2.44.2") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/CredentialsStateManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/CredentialsStateManager.kt index 2158182..3ef5156 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/CredentialsStateManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/CredentialsStateManager.kt @@ -24,9 +24,11 @@ sealed class CredentialsState { object None : CredentialsState() sealed class CredentialsRequested : CredentialsState() object SshCredentialsRequested : CredentialsRequested() + object GpgCredentialsRequested : CredentialsRequested() object HttpCredentialsRequested : CredentialsRequested() object CredentialsDenied : CredentialsState() sealed class CredentialsAccepted : CredentialsState() data class SshCredentialsAccepted(val password: String) : CredentialsAccepted() + data class GpgCredentialsAccepted(val password: String) : CredentialsAccepted() data class HttpCredentialsAccepted(val user: String, val password: String) : CredentialsAccepted() } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/GpgCredentialsProvider.kt b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/GpgCredentialsProvider.kt new file mode 100644 index 0000000..aa78b87 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/GpgCredentialsProvider.kt @@ -0,0 +1,43 @@ +package com.jetpackduba.gitnuro.credentials + +import org.eclipse.jgit.transport.CredentialItem +import org.eclipse.jgit.transport.CredentialsProvider +import org.eclipse.jgit.transport.URIish +import javax.inject.Inject + +private const val PASSWORD_FIELD_IDENTIFIER = "Passphrase" + +class GpgCredentialsProvider @Inject constructor( + private val credentialsStateManager: CredentialsStateManager, +) : CredentialsProvider() { + override fun isInteractive(): Boolean = true + + override fun supports(vararg items: CredentialItem?): Boolean { + println(items) + return true + } + + override fun get(uri: URIish?, vararg items: CredentialItem?): Boolean { + val item = items.firstOrNull { + it?.promptText == PASSWORD_FIELD_IDENTIFIER + } + + if (item != null && item is CredentialItem.Password) { + credentialsStateManager.updateState(CredentialsState.GpgCredentialsRequested) + + var credentials = credentialsStateManager.currentCredentialsState + + while (credentials is CredentialsState.GpgCredentialsRequested) { + credentials = credentialsStateManager.currentCredentialsState + } + + if (credentials is CredentialsState.GpgCredentialsAccepted) { + item.value = credentials.password.toCharArray() + return true + } + } + + return false + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/workspace/DoCommitUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/workspace/DoCommitUseCase.kt index 4b8fa77..ced6363 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/workspace/DoCommitUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/workspace/DoCommitUseCase.kt @@ -1,17 +1,24 @@ package com.jetpackduba.gitnuro.git.workspace +import com.jetpackduba.gitnuro.credentials.GpgCredentialsProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git import org.eclipse.jgit.revwalk.RevCommit +import org.eclipse.jgit.transport.CredentialItem +import org.eclipse.jgit.transport.CredentialsProvider +import org.eclipse.jgit.transport.URIish import javax.inject.Inject -class DoCommitUseCase @Inject constructor() { +class DoCommitUseCase @Inject constructor( + private val gpgCredentialsProvider: GpgCredentialsProvider, +) { suspend operator fun invoke(git: Git, message: String, amend: Boolean): RevCommit = withContext(Dispatchers.IO) { git.commit() .setMessage(message) .setAllowEmpty(amend) // Only allow empty commits when amending .setAmend(amend) + .setCredentialsProvider(gpgCredentialsProvider) .call() } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/AppTab.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/AppTab.kt index 3a3b178..9074d47 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/AppTab.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/AppTab.kt @@ -22,9 +22,7 @@ import androidx.compose.ui.unit.sp import com.jetpackduba.gitnuro.LoadingRepository import com.jetpackduba.gitnuro.LocalTabScope import com.jetpackduba.gitnuro.credentials.CredentialsState -import com.jetpackduba.gitnuro.ui.dialogs.CloneDialog -import com.jetpackduba.gitnuro.ui.dialogs.PasswordDialog -import com.jetpackduba.gitnuro.ui.dialogs.UserPasswordDialog +import com.jetpackduba.gitnuro.ui.dialogs.* import com.jetpackduba.gitnuro.ui.dialogs.settings.SettingsDialog import com.jetpackduba.gitnuro.viewmodels.RepositorySelectionStatus import com.jetpackduba.gitnuro.viewmodels.TabViewModel @@ -191,7 +189,7 @@ fun CredentialsDialog(gitManager: TabViewModel) { } ) } else if (credentialsState == CredentialsState.SshCredentialsRequested) { - PasswordDialog( + SshPasswordDialog( onReject = { gitManager.credentialsDenied() }, @@ -199,5 +197,14 @@ fun CredentialsDialog(gitManager: TabViewModel) { gitManager.sshCredentialsAccepted(password) } ) + } else if (credentialsState == CredentialsState.GpgCredentialsRequested) { + GpgPasswordDialog( + onReject = { + gitManager.credentialsDenied() + }, + onAccept = { password -> + gitManager.gpgCredentialsAccepted(password) + } + ) } } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/GpgPasswordDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/GpgPasswordDialog.kt new file mode 100644 index 0000000..e344be2 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/GpgPasswordDialog.kt @@ -0,0 +1,38 @@ +package com.jetpackduba.gitnuro.ui.dialogs + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.jetpackduba.gitnuro.keybindings.KeybindingOption +import com.jetpackduba.gitnuro.keybindings.matchesBinding +import com.jetpackduba.gitnuro.theme.outlinedTextFieldColors +import com.jetpackduba.gitnuro.theme.onBackgroundSecondary +import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField +import com.jetpackduba.gitnuro.ui.components.PrimaryButton + +@Composable +fun GpgPasswordDialog( + onReject: () -> Unit, + onAccept: (password: String) -> Unit +) { + PasswordDialog( + "Introduce your GPG key's password", + "Your GPG key is protected with a password", + "key.svg", + onReject, + onAccept, + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/PasswordDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/PasswordDialog.kt index 5d62f25..835c572 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/PasswordDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/PasswordDialog.kt @@ -25,8 +25,11 @@ import com.jetpackduba.gitnuro.ui.components.PrimaryButton @Composable fun PasswordDialog( + title: String, + subtitle: String, + icon: String, onReject: () -> Unit, - onAccept: (password: String) -> Unit + onAccept: (password: String) -> Unit, ) { var passwordField by remember { mutableStateOf("") } val passwordFieldFocusRequester = remember { FocusRequester() } @@ -39,7 +42,7 @@ fun PasswordDialog( ) { Icon( - painterResource("lock.svg"), + painterResource(icon), contentDescription = null, modifier = Modifier .size(64.dp) @@ -48,7 +51,7 @@ fun PasswordDialog( ) Text( - text = "Introduce your SSH key's password", + text = title, modifier = Modifier .padding(bottom = 8.dp), color = MaterialTheme.colors.onBackground, @@ -56,7 +59,7 @@ fun PasswordDialog( ) Text( - text = "Your SSH key is protected with a password", + text = subtitle, modifier = Modifier .padding(bottom = 16.dp), color = MaterialTheme.colors.onBackgroundSecondary, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/SshPasswordDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/SshPasswordDialog.kt new file mode 100644 index 0000000..6ce80c3 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/SshPasswordDialog.kt @@ -0,0 +1,38 @@ +package com.jetpackduba.gitnuro.ui.dialogs + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.jetpackduba.gitnuro.keybindings.KeybindingOption +import com.jetpackduba.gitnuro.keybindings.matchesBinding +import com.jetpackduba.gitnuro.theme.outlinedTextFieldColors +import com.jetpackduba.gitnuro.theme.onBackgroundSecondary +import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField +import com.jetpackduba.gitnuro.ui.components.PrimaryButton + +@Composable +fun SshPasswordDialog( + onReject: () -> Unit, + onAccept: (password: String) -> Unit +) { + PasswordDialog( + "Introduce your SSH key's password", + "Your SSH key is protected with a password", + "lock.svg", + onReject, + onAccept, + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt index 7470ceb..9ab67ca 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt @@ -441,6 +441,10 @@ class TabViewModel @Inject constructor( abortRebaseUseCase(git) rebaseInteractiveViewModel = null // shouldn't be necessary but just to make sure } + + fun gpgCredentialsAccepted(password: String) { + credentialsStateManager.updateState(CredentialsState.GpgCredentialsAccepted(password)) + } } diff --git a/src/main/resources/key.svg b/src/main/resources/key.svg new file mode 100644 index 0000000..d92ef4a --- /dev/null +++ b/src/main/resources/key.svg @@ -0,0 +1 @@ + \ No newline at end of file