From 3840f43fa0026b06b15913379761c1bf6b6a1c68 Mon Sep 17 00:00:00 2001 From: Fabian Henneke Date: Thu, 3 Sep 2020 10:48:14 +0200 Subject: [PATCH] Refactor Git operations and auth (#1066) Co-authored-by: Harsh Shandilya --- app/build.gradle.kts | 1 + app/proguard-rules.pro | 1 + app/src/main/AndroidManifest.xml | 4 - .../java/com/zeapo/pwdstore/LaunchActivity.kt | 10 +- .../com/zeapo/pwdstore/PasswordFragment.kt | 18 ++- .../java/com/zeapo/pwdstore/PasswordStore.kt | 56 +++----- .../autofill/oreo/ui/AutofillSaveActivity.kt | 16 +-- .../crypto/PasswordCreationActivity.kt | 39 +++--- .../com/zeapo/pwdstore/git/BaseGitActivity.kt | 104 ++++++++++---- .../zeapo/pwdstore/git/GitCommandExecutor.kt | 86 ++---------- .../zeapo/pwdstore/git/GitConfigActivity.kt | 31 ++++- .../pwdstore/git/GitOperationActivity.kt | 79 ----------- .../pwdstore/git/GitServerConfigActivity.kt | 13 +- .../git/operation/BreakOutOfDetached.kt | 30 +--- .../pwdstore/git/operation/CloneOperation.kt | 20 +-- .../pwdstore/git/operation/GitOperation.kt | 130 ++++++++---------- .../pwdstore/git/operation/PullOperation.kt | 17 +-- .../pwdstore/git/operation/PushOperation.kt | 18 +-- .../git/operation/ResetToRemoteOperation.kt | 14 +- .../pwdstore/git/operation/SyncOperation.kt | 14 +- .../dialogs/FolderCreationDialogFragment.kt | 1 - .../com/zeapo/pwdstore/utils/Extensions.kt | 29 ++-- .../java/com/zeapo/pwdstore/utils/Result.kt | 16 --- .../main/res/layout/activity_git_clone.xml | 2 +- app/src/main/res/menu/git_clone.xml | 15 -- app/src/main/res/values/strings.xml | 2 + build.gradle.kts | 2 +- buildSrc/src/main/java/Dependencies.kt | 1 + 28 files changed, 276 insertions(+), 493 deletions(-) delete mode 100644 app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/Result.kt delete mode 100644 app/src/main/res/menu/git_clone.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e613b79a..2d5c7262 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -118,6 +118,7 @@ dependencies { implementation(Dependencies.ThirdParty.jgit) { exclude(group = "org.apache.httpcomponents", module = "httpclient") } + implementation(Dependencies.ThirdParty.kotlin_result) implementation(Dependencies.ThirdParty.sshj) implementation(Dependencies.ThirdParty.bouncycastle) implementation(Dependencies.ThirdParty.plumber) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index eaa3d830..753f02ab 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,6 +18,7 @@ -keepattributes SourceFile,LineNumberTable -dontobfuscate +-keep class com.jcraft.jsch.** -keep class org.eclipse.jgit.internal.JGitText { *; } -keep class org.bouncycastle.jcajce.provider.** { *; } -keep class org.bouncycastle.jce.provider.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cbacda10..a9fa83d7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -51,10 +51,6 @@ android:windowSoftInputMode="stateAlwaysHidden" tools:node="replace" /> - - BaseGitActivity.REQUEST_PULL else -> BaseGitActivity.REQUEST_SYNC } - val intent = Intent(context, GitOperationActivity::class.java) - intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, operationId) - swipeResult.launch(intent) + requireStore().apply { + lifecycleScope.launch { + launchGitOperation(operationId).fold( + success = { + binding.swipeRefresher.isRefreshing = false + refreshPasswordList() + }, + failure = ::finishAfterPromptOnErrorHandler, + ) + } + } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt index d9afc7c9..7f8c4e55 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt @@ -23,7 +23,6 @@ import android.view.MenuItem.OnActionExpandListener import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView.OnQueryTextListener @@ -38,6 +37,7 @@ import com.github.ajalt.timberkt.d import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.i import com.github.ajalt.timberkt.w +import com.github.michaelbull.result.fold import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText @@ -48,7 +48,6 @@ import com.zeapo.pwdstore.crypto.BasePgpActivity.Companion.getLongName import com.zeapo.pwdstore.crypto.DecryptActivity import com.zeapo.pwdstore.crypto.PasswordCreationActivity import com.zeapo.pwdstore.git.BaseGitActivity -import com.zeapo.pwdstore.git.GitOperationActivity import com.zeapo.pwdstore.git.GitServerConfigActivity import com.zeapo.pwdstore.git.config.AuthMode import com.zeapo.pwdstore.git.config.GitSettings @@ -81,7 +80,7 @@ import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.errors.GitAPIException import org.eclipse.jgit.revwalk.RevCommit -class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { +class PasswordStore : BaseGitActivity() { private lateinit var activity: PasswordStore private lateinit var searchItem: MenuItem @@ -101,12 +100,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { } } - private val listRefreshAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - refreshPasswordList() - } - } - private val repositoryInitAction = registerForActivityResult(StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { initializeRepositoryInfo() @@ -136,7 +129,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { private val checkPermissionsAndCloneAction = registerForActivityResult(RequestPermission()) { granted -> if (granted) { val intent = Intent(activity, GitServerConfigActivity::class.java) - intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE) + intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE) cloneAction.launch(intent) } } @@ -163,10 +156,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { @SuppressLint("NewApi") override fun onCreate(savedInstanceState: Bundle?) { activity = this - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - shortcutManager = getSystemService() - } - // If user opens app with permission granted then revokes and returns, // prevent attempt to create password list fragment var savedInstance = savedInstanceState @@ -177,6 +166,10 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { savedInstance = null } super.onCreate(savedInstance) + setContentView(R.layout.activity_pwdstore) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + shortcutManager = getSystemService() + } // If user is eligible for Oreo autofill, prompt them to switch. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && @@ -328,9 +321,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { initBefore.show() return false } - intent = Intent(this, GitOperationActivity::class.java) - intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_PUSH) - startActivity(intent) + runGitOperation(REQUEST_PUSH) return true } R.id.git_pull -> { @@ -338,9 +329,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { initBefore.show() return false } - intent = Intent(this, GitOperationActivity::class.java) - intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_PULL) - listRefreshAction.launch(intent) + runGitOperation(REQUEST_PULL) return true } R.id.git_sync -> { @@ -348,9 +337,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { initBefore.show() return false } - intent = Intent(this, GitOperationActivity::class.java) - intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_SYNC) - listRefreshAction.launch(intent) + runGitOperation(REQUEST_SYNC) return true } R.id.refresh -> { @@ -415,6 +402,13 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { createRepository() } + private fun runGitOperation(operation: Int) = lifecycleScope.launch { + launchGitOperation(operation).fold( + success = { refreshPasswordList() }, + failure = ::finishAfterPromptOnErrorHandler, + ) + } + /** * Validates if storage permission is granted, and requests for it if not. The return value * is true if the permission has been granted. @@ -576,12 +570,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { intent.putExtra("REPO_PATH", getRepositoryDirectory().absolutePath) registerForActivityResult(StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { - lifecycleScope.launch { - commitChange( - resources.getString(R.string.git_commit_add_text, result.data?.extras?.getString("LONG_NAME")), - finishActivityOnEnd = false, - ) - } refreshPasswordList() } }.launch(intent) @@ -624,7 +612,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { lifecycleScope.launch { commitChange( resources.getString(R.string.git_commit_remove_text, fmt), - finishActivityOnEnd = false, ) } } @@ -636,7 +623,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { val intent = Intent(this, SelectFolderActivity::class.java) val fileLocations = values.map { it.file.absolutePath }.toTypedArray() intent.putExtra("Files", fileLocations) - intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, "SELECTFOLDER") + intent.putExtra(REQUEST_ARG_OP, "SELECTFOLDER") registerForActivityResult(StartActivityForResult()) { result -> val intentData = result.data ?: return@registerForActivityResult val filesToMove = requireNotNull(intentData.getStringArrayExtra("Files")) @@ -694,7 +681,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { withContext(Dispatchers.Main) { commitChange( resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName), - finishActivityOnEnd = false, ) } } @@ -704,7 +690,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { withContext(Dispatchers.Main) { commitChange( resources.getString(R.string.git_commit_move_multiple_text, relativePath), - finishActivityOnEnd = false, ) } } @@ -769,7 +754,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { withContext(Dispatchers.Main) { commitChange( resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name), - finishActivityOnEnd = false, ) } } @@ -848,7 +832,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { NEW_REPO_BUTTON -> initializeRepositoryInfo() CLONE_REPO_BUTTON -> { val intent = Intent(activity, GitServerConfigActivity::class.java) - intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE) + intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE) cloneAction.launch(intent) } } @@ -872,7 +856,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { NEW_REPO_BUTTON -> initializeRepositoryInfo() CLONE_REPO_BUTTON -> { val intent = Intent(activity, GitServerConfigActivity::class.java) - intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE) + intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE) cloneAction.launch(intent) } } diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt index f6ef8757..0052ff65 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt @@ -15,9 +15,7 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.os.bundleOf -import androidx.lifecycle.lifecycleScope import com.github.ajalt.timberkt.e -import com.zeapo.pwdstore.R import com.zeapo.pwdstore.autofill.oreo.AutofillAction import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences @@ -26,9 +24,7 @@ import com.zeapo.pwdstore.autofill.oreo.FillableForm import com.zeapo.pwdstore.autofill.oreo.FormOrigin import com.zeapo.pwdstore.crypto.PasswordCreationActivity import com.zeapo.pwdstore.utils.PasswordRepository -import com.zeapo.pwdstore.utils.commitChange import java.io.File -import kotlinx.coroutines.launch @RequiresApi(Build.VERSION_CODES.O) class AutofillSaveActivity : AppCompatActivity() { @@ -119,7 +115,6 @@ class AutofillSaveActivity : AppCompatActivity() { formOrigin?.let { AutofillMatcher.addMatchFor(this, it, File(createdPath)) } - val longName = data.getStringExtra("LONG_NAME")!! val password = data.getStringExtra("PASSWORD") val resultIntent = if (password != null) { // Password was generated and should be filled into a form. @@ -144,15 +139,8 @@ class AutofillSaveActivity : AppCompatActivity() { // Password was extracted from a form, there is nothing to fill. Intent() } - // PasswordCreationActivity delegates committing the added file to PasswordStore. Since - // PasswordStore is not involved in an AutofillScenario, we have to commit the file ourselves. - lifecycleScope.launch { - commitChange( - getString(R.string.git_commit_add_text, longName), - finishWithResultOnEnd = resultIntent - ) - } - // GitAsyncTask will finish the activity for us. + setResult(RESULT_OK, resultIntent) + finish() } }.launch(saveIntent) } diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt index ff5a8179..368182ee 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt @@ -20,6 +20,7 @@ import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.lifecycle.lifecycleScope import com.github.ajalt.timberkt.e +import com.github.michaelbull.result.onSuccess import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator.QR_CODE @@ -331,14 +332,13 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> gpgIdentifierFile.writeText(keyIds.joinToString("\n")) lifecycleScope.launch { - commitChange( - getString( - R.string.git_commit_gpg_id, - getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) - ) - ) + commitChange(getString( + R.string.git_commit_gpg_id, + getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) + )).onSuccess { + encrypt(data) + } } - encrypt(data) } } }.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java)) @@ -440,17 +440,6 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB returnIntent.putExtra(RETURN_EXTRA_USERNAME, username) } - if (editing) { - lifecycleScope.launch { - commitChange( - getString( - R.string.git_commit_edit_text, - getLongName(fullPath, repoPath, editName) - ) - ) - } - } - if (directoryInputLayout.isVisible && directoryInputLayout.isEnabled && oldFileName != null) { val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg") if (oldFile.path != file.path && !oldFile.delete()) { @@ -463,13 +452,19 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB finish() } .show() - } else { + return@executeApiAsync + } + } + + val commitMessageRes = if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text + lifecycleScope.launch { + commitChange(resources.getString( + commitMessageRes, + getLongName(fullPath, repoPath, editName) + )).onSuccess { setResult(RESULT_OK, returnIntent) finish() } - } else { - setResult(RESULT_OK, returnIntent) - finish() } } catch (e: Exception) { diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt index ed72c88d..e16b0c8b 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt @@ -6,9 +6,14 @@ package com.zeapo.pwdstore.git import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit import com.github.ajalt.timberkt.Timber.tag +import com.github.ajalt.timberkt.d import com.github.ajalt.timberkt.e +import com.github.michaelbull.result.Err import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.github.michaelbull.result.Result +import com.zeapo.pwdstore.R import com.zeapo.pwdstore.git.config.GitSettings import com.zeapo.pwdstore.git.operation.BreakOutOfDetached import com.zeapo.pwdstore.git.operation.CloneOperation @@ -17,10 +22,15 @@ import com.zeapo.pwdstore.git.operation.PullOperation import com.zeapo.pwdstore.git.operation.PushOperation import com.zeapo.pwdstore.git.operation.ResetToRemoteOperation import com.zeapo.pwdstore.git.operation.SyncOperation -import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.PreferenceKeys +import com.zeapo.pwdstore.utils.getEncryptedPrefs +import com.zeapo.pwdstore.utils.sharedPrefs +import net.schmizz.sshj.common.DisconnectReason +import net.schmizz.sshj.common.SSHException +import net.schmizz.sshj.userauth.UserAuthException /** - * Abstract AppCompatActivity that holds some information that is commonly shared across git-related + * Abstract [AppCompatActivity] that holds some information that is commonly shared across git-related * tasks and makes sense to be held here. */ abstract class BaseGitActivity : AppCompatActivity() { @@ -39,33 +49,77 @@ abstract class BaseGitActivity : AppCompatActivity() { * Attempt to launch the requested Git operation. * @param operation The type of git operation to launch */ - suspend fun launchGitOperation(operation: Int) { + suspend fun launchGitOperation(operation: Int): Result { if (GitSettings.url == null) { - setResult(RESULT_CANCELED) - finish() - return + return Err(IllegalStateException("Git url is not set!")) } - try { - val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory()) - val op = when (operation) { - REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, GitSettings.url!!, this) - REQUEST_PULL -> PullOperation(localDir, this) - REQUEST_PUSH -> PushOperation(localDir, this) - REQUEST_SYNC -> SyncOperation(localDir, this) - BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(localDir, this) - REQUEST_RESET -> ResetToRemoteOperation(localDir, this) - else -> { - tag(TAG).e { "Operation not recognized : $operation" } - setResult(RESULT_CANCELED) - finish() - return - } + val op = when (operation) { + REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(this, GitSettings.url!!) + REQUEST_PULL -> PullOperation(this) + REQUEST_PUSH -> PushOperation(this) + REQUEST_SYNC -> SyncOperation(this) + BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(this) + REQUEST_RESET -> ResetToRemoteOperation(this) + else -> { + tag(TAG).e { "Operation not recognized : $operation" } + return Err(IllegalArgumentException("$operation is not a valid Git operation")) } - op.executeAfterAuthentication(GitSettings.authMode) - } catch (e: Exception) { - e.printStackTrace() - MaterialAlertDialogBuilder(this).setMessage(e.message).show() } + return op.executeAfterAuthentication(GitSettings.authMode) + } + + fun finishOnSuccessHandler(@Suppress("UNUSED_PARAMETER") nothing: Unit) { + finish() + } + + fun finishAfterPromptOnErrorHandler(err: Throwable) { + val error = rootCauseException(err) + if (!isExplicitlyUserInitiatedError(error)) { + getEncryptedPrefs("git_operation").edit { + remove(PreferenceKeys.HTTPS_PASSWORD) + } + sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) } + d(error) + MaterialAlertDialogBuilder(this) + .setTitle(resources.getString(R.string.jgit_error_dialog_title)) + .setMessage(ErrorMessages[error]) + .setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> + finish() + }.show() + } + } + + /** + * Check if a given [Throwable] is the result of an error caused by the user cancelling the + * operation. + */ + private fun isExplicitlyUserInitiatedError(throwable: Throwable): Boolean { + var cause: Throwable? = throwable + while (cause != null) { + if (cause is SSHException && + cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER) + return true + cause = cause.cause + } + return false + } + + /** + * Get the real root cause of a [Throwable] by traversing until known wrapping exceptions are no + * longer found. + */ + private fun rootCauseException(throwable: Throwable): Throwable { + var rootCause = throwable + // JGit's TransportException hides the more helpful SSHJ exceptions. + // Also, SSHJ's UserAuthException about exhausting available authentication methods hides + // more useful exceptions. + while ((rootCause is org.eclipse.jgit.errors.TransportException || + rootCause is org.eclipse.jgit.api.errors.TransportException || + (rootCause is UserAuthException && + rootCause.message == "Exhausted available authentication methods"))) { + rootCause = rootCause.cause ?: break + } + return rootCause } companion object { diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt index d5956b1f..8d8648bf 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt @@ -5,25 +5,18 @@ package com.zeapo.pwdstore.git -import android.app.Activity -import android.content.Intent import android.widget.Toast import androidx.fragment.app.FragmentActivity -import com.github.ajalt.timberkt.e import com.google.android.material.snackbar.Snackbar import com.zeapo.pwdstore.R import com.zeapo.pwdstore.git.GitException.PullException import com.zeapo.pwdstore.git.GitException.PushException import com.zeapo.pwdstore.git.config.GitSettings import com.zeapo.pwdstore.git.operation.GitOperation -import com.zeapo.pwdstore.git.sshj.SshjSessionFactory -import com.zeapo.pwdstore.utils.Result import com.zeapo.pwdstore.utils.snackbar +import com.github.michaelbull.result.Result import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import net.schmizz.sshj.common.DisconnectReason -import net.schmizz.sshj.common.SSHException -import net.schmizz.sshj.userauth.UserAuthException import org.eclipse.jgit.api.CommitCommand import org.eclipse.jgit.api.PullCommand import org.eclipse.jgit.api.PushCommand @@ -31,25 +24,20 @@ import org.eclipse.jgit.api.RebaseResult import org.eclipse.jgit.api.StatusCommand import org.eclipse.jgit.lib.PersonIdent import org.eclipse.jgit.transport.RemoteRefUpdate -import org.eclipse.jgit.transport.SshSessionFactory class GitCommandExecutor( private val activity: FragmentActivity, private val operation: GitOperation, - private val finishWithResultOnEnd: Intent? = Intent(), - private val finishActivityOnEnd: Boolean = true, ) { - suspend fun execute() { - operation.setCredentialProvider() + suspend fun execute(): Result { val snackbar = activity.snackbar( message = activity.resources.getString(R.string.git_operation_running), length = Snackbar.LENGTH_INDEFINITE, ) // Count the number of uncommitted files var nbChanges = 0 - var operationResult: Result = Result.Ok - try { + return com.github.michaelbull.result.runCatching { for (command in operation.commands) { when (command) { is StatusCommand -> { @@ -74,7 +62,7 @@ class GitCommandExecutor( } val rr = result.rebaseResult if (rr.status === RebaseResult.Status.STOPPED) { - operationResult = Result.Err(PullException.PullRebaseFailed) + throw PullException.PullRebaseFailed } } is PushCommand -> { @@ -84,15 +72,15 @@ class GitCommandExecutor( for (result in results) { // Code imported (modified) from Gerrit PushOp, license Apache v2 for (rru in result.remoteUpdates) { - val error = when (rru.status) { - RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> PushException.NonFastForward + when (rru.status) { + RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> throw PushException.NonFastForward RemoteRefUpdate.Status.REJECTED_NODELETE, RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED, RemoteRefUpdate.Status.NON_EXISTING, RemoteRefUpdate.Status.NOT_ATTEMPTED, - -> PushException.Generic(rru.status.name) + -> throw PushException.Generic(rru.status.name) RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> { - if ("non-fast-forward" == rru.message) { + throw if ("non-fast-forward" == rru.message) { PushException.RemoteRejected } else { PushException.Generic(rru.message) @@ -106,13 +94,7 @@ class GitCommandExecutor( Toast.LENGTH_SHORT ).show() } - null } - else -> null - - } - if (error != null) { - operationResult = Result.Err(error) } } } @@ -124,56 +106,8 @@ class GitCommandExecutor( } } } - } catch (e: Exception) { - operationResult = Result.Err(e) + }.also { + snackbar.dismiss() } - when (operationResult) { - is Result.Err -> { - activity.setResult(Activity.RESULT_CANCELED) - if (isExplicitlyUserInitiatedError(operationResult.err)) { - // Currently, this is only executed when the user cancels a password prompt - // during authentication. - if (finishActivityOnEnd) activity.finish() - } else { - e(operationResult.err) - operation.onError(rootCauseException(operationResult.err)) - } - } - is Result.Ok -> { - operation.onSuccess() - activity.setResult(Activity.RESULT_OK, finishWithResultOnEnd) - if (finishActivityOnEnd) activity.finish() - } - } - snackbar.dismiss() - withContext(Dispatchers.IO) { - (SshSessionFactory.getInstance() as? SshjSessionFactory)?.close() - } - SshSessionFactory.setInstance(null) - } - - private fun isExplicitlyUserInitiatedError(e: Exception): Boolean { - var cause: Exception? = e - while (cause != null) { - if (cause is SSHException && - cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER) - return true - cause = cause.cause as? Exception - } - return false - } - - private fun rootCauseException(e: Exception): Exception { - var rootCause = e - // JGit's TransportException hides the more helpful SSHJ exceptions. - // Also, SSHJ's UserAuthException about exhausting available authentication methods hides - // more useful exceptions. - while ((rootCause is org.eclipse.jgit.errors.TransportException || - rootCause is org.eclipse.jgit.api.errors.TransportException || - (rootCause is UserAuthException && - rootCause.message == "Exhausted available authentication methods"))) { - rootCause = rootCause.cause as? Exception ?: break - } - return rootCause } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt index 89376bfd..c6ab3ca3 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt @@ -11,6 +11,7 @@ import android.util.Patterns import androidx.core.os.postDelayed import androidx.lifecycle.lifecycleScope import com.github.ajalt.timberkt.e +import com.github.michaelbull.result.fold import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.zeapo.pwdstore.R @@ -75,8 +76,34 @@ class GitConfigActivity : BaseGitActivity() { e(ex) { "Failed to start GitLogActivity" } } } - binding.gitAbortRebase.setOnClickListener { lifecycleScope.launch { launchGitOperation(BREAK_OUT_OF_DETACHED) } } - binding.gitResetToRemote.setOnClickListener { lifecycleScope.launch { launchGitOperation(REQUEST_RESET) } } + binding.gitAbortRebase.setOnClickListener { + lifecycleScope.launch { + launchGitOperation(BREAK_OUT_OF_DETACHED).fold( + success = { + MaterialAlertDialogBuilder(this@GitConfigActivity) + .setTitle(resources.getString(R.string.git_abort_and_push_title)) + .setMessage(resources.getString( + R.string.git_break_out_of_detached_success, + GitSettings.branch, + "conflicting-${GitSettings.branch}-...", + )) + .setOnCancelListener { finish() } + .setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> + finish() + }.show() + }, + failure = ::finishAfterPromptOnErrorHandler, + ) + } + } + binding.gitResetToRemote.setOnClickListener { + lifecycleScope.launch { + launchGitOperation(REQUEST_RESET).fold( + success = ::finishOnSuccessHandler, + failure = ::finishAfterPromptOnErrorHandler, + ) + } + } } /** diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt deleted file mode 100644 index 8b772747..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package com.zeapo.pwdstore.git - -import android.content.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import androidx.lifecycle.lifecycleScope -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.zeapo.pwdstore.R -import com.zeapo.pwdstore.UserPreference -import com.zeapo.pwdstore.git.config.GitSettings -import com.zeapo.pwdstore.utils.PasswordRepository -import kotlinx.coroutines.launch - -open class GitOperationActivity : BaseGitActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - when (val reqCode = intent.extras?.getInt(REQUEST_ARG_OP)) { - REQUEST_PULL -> lifecycleScope.launch { syncRepository(REQUEST_PULL) } - REQUEST_PUSH -> lifecycleScope.launch { syncRepository(REQUEST_PUSH) } - REQUEST_SYNC -> lifecycleScope.launch { syncRepository(REQUEST_SYNC) } - else -> { - throw IllegalArgumentException("Invalid request code: $reqCode") - } - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - // Inflate the menu; this adds items to the action bar if it is present. - menuInflater.inflate(R.menu.git_clone, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.user_pref -> try { - val intent = Intent(this, UserPreference::class.java) - startActivity(intent) - true - } catch (e: Exception) { - println("Exception caught :(") - e.printStackTrace() - false - } - else -> super.onOptionsItemSelected(item) - } - } - - /** - * Syncs the local repository with the remote one (either pull or push) - * - * @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH - */ - private suspend fun syncRepository(operation: Int) { - if (GitSettings.url.isNullOrEmpty()) - MaterialAlertDialogBuilder(this) - .setMessage(getString(R.string.set_information_dialog_text)) - .setPositiveButton(getString(R.string.dialog_positive)) { _, _ -> - val intent = Intent(this, UserPreference::class.java) - startActivityForResult(intent, REQUEST_PULL) - } - .setNegativeButton(getString(R.string.dialog_negative)) { _, _ -> - // do nothing :( - setResult(RESULT_OK) - finish() - } - .show() - else { - // check that the remote origin is here, else add it - PasswordRepository.addRemote("origin", GitSettings.url!!, true) - launchGitOperation(operation) - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt index 5aa34201..27cd6379 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt @@ -9,6 +9,7 @@ import android.os.Handler import android.view.View import androidx.core.os.postDelayed import androidx.lifecycle.lifecycleScope +import com.github.michaelbull.result.fold import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.zeapo.pwdstore.R @@ -122,7 +123,10 @@ class GitServerConfigActivity : BaseGitActivity() { localDir.deleteRecursively() } snackbar.dismiss() - launchGitOperation(REQUEST_CLONE) + launchGitOperation(REQUEST_CLONE).fold( + success = ::finishOnSuccessHandler, + failure = ::finishAfterPromptOnErrorHandler, + ) } } catch (e: IOException) { // TODO Handle the exception correctly if we are unable to delete the directory... @@ -153,7 +157,12 @@ class GitServerConfigActivity : BaseGitActivity() { e.printStackTrace() MaterialAlertDialogBuilder(this).setMessage(e.message).show() } - lifecycleScope.launch { launchGitOperation(REQUEST_CLONE) } + lifecycleScope.launch { + launchGitOperation(REQUEST_CLONE).fold( + success = ::finishOnSuccessHandler, + failure = ::finishAfterPromptOnErrorHandler, + ) + } } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt index b46b66cf..6c7fee3a 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt @@ -7,46 +7,30 @@ package com.zeapo.pwdstore.git.operation import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.R -import com.zeapo.pwdstore.git.GitCommandExecutor -import java.io.File import org.eclipse.jgit.api.RebaseCommand -class BreakOutOfDetached(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { - - private val branchName = "conflicting-$remoteBranch-${System.currentTimeMillis()}" +class BreakOutOfDetached(callingActivity: AppCompatActivity) : GitOperation(callingActivity) { override val commands = arrayOf( // abort the rebase git.rebase().setOperation(RebaseCommand.Operation.ABORT), // git checkout -b conflict-branch - git.checkout().setCreateBranch(true).setName(branchName), + git.checkout().setCreateBranch(true).setName("conflicting-$remoteBranch-${System.currentTimeMillis()}"), // push the changes git.push().setRemote("origin"), // switch back to ${gitBranch} git.checkout().setName(remoteBranch), ) - override suspend fun execute() { - if (!git.repository.repositoryState.isRebasing) { - MaterialAlertDialogBuilder(callingActivity) - .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title)) - .setMessage("The repository is not rebasing, no need to push to another branch") - .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> - callingActivity.finish() - }.show() - return - } - GitCommandExecutor(callingActivity, this).execute() - } - - override fun onSuccess() { + override fun preExecute() = if (!git.repository.repositoryState.isRebasing) { MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title)) - .setMessage("There was a conflict when trying to rebase. " + - "Your local $remoteBranch branch was pushed to another branch named conflicting-$remoteBranch-....\n" + - "Use this branch to resolve conflict on your computer") + .setMessage(callingActivity.resources.getString(R.string.git_break_out_of_detached_unneeded)) .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() }.show() + false + } else { + true } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt index a032a669..256437a8 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt @@ -5,34 +5,18 @@ package com.zeapo.pwdstore.git.operation import androidx.appcompat.app.AppCompatActivity -import com.zeapo.pwdstore.git.GitCommandExecutor -import java.io.File import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.GitCommand /** * Creates a new clone operation * - * @param fileDir the git working tree directory * @param uri URL to clone the repository from * @param callingActivity the calling activity */ -class CloneOperation(fileDir: File, uri: String, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { +class CloneOperation(callingActivity: AppCompatActivity, uri: String) : GitOperation(callingActivity) { override val commands: Array> = arrayOf( - Git.cloneRepository().setBranch(remoteBranch).setDirectory(repository?.workTree).setURI(uri), + Git.cloneRepository().setBranch(remoteBranch).setDirectory(repository.workTree).setURI(uri), ) - - override suspend fun execute() { - GitCommandExecutor(callingActivity, this, finishActivityOnEnd = false).execute() - } - - override fun onSuccess() { - callingActivity.finish() - } - - override fun onError(err: Exception) { - finishFromErrorDialog = false - super.onError(err) - } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt index 1b47d79b..2a72005e 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt @@ -6,30 +6,27 @@ package com.zeapo.pwdstore.git.operation import android.content.Intent import android.widget.Toast -import androidx.annotation.CallSuper -import androidx.core.content.edit import androidx.fragment.app.FragmentActivity -import com.github.ajalt.timberkt.Timber.d +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.R import com.zeapo.pwdstore.UserPreference -import com.zeapo.pwdstore.git.ErrorMessages +import com.zeapo.pwdstore.git.GitCommandExecutor import com.zeapo.pwdstore.git.config.AuthMode import com.zeapo.pwdstore.git.config.GitSettings -import com.zeapo.pwdstore.git.sshj.InteractivePasswordFinder import com.zeapo.pwdstore.git.sshj.SshAuthData import com.zeapo.pwdstore.git.sshj.SshKey import com.zeapo.pwdstore.git.sshj.SshjSessionFactory import com.zeapo.pwdstore.utils.BiometricAuthenticator import com.zeapo.pwdstore.utils.PasswordRepository -import com.zeapo.pwdstore.utils.PreferenceKeys -import com.zeapo.pwdstore.utils.getEncryptedPrefs -import com.zeapo.pwdstore.utils.sharedPrefs -import java.io.File import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import net.schmizz.sshj.common.DisconnectReason +import net.schmizz.sshj.common.SSHException import net.schmizz.sshj.userauth.password.PasswordFinder import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.GitCommand @@ -37,24 +34,22 @@ import org.eclipse.jgit.api.TransportCommand import org.eclipse.jgit.errors.UnsupportedCredentialItem import org.eclipse.jgit.transport.CredentialItem import org.eclipse.jgit.transport.CredentialsProvider -import org.eclipse.jgit.transport.SshSessionFactory +import org.eclipse.jgit.transport.SshTransport +import org.eclipse.jgit.transport.Transport import org.eclipse.jgit.transport.URIish -const val ANDROID_KEYSTORE_ALIAS_SSH_KEY = "ssh_key" - /** * Creates a new git operation * - * @param gitDir the git working tree directory * @param callingActivity the calling activity */ -abstract class GitOperation(gitDir: File, internal val callingActivity: FragmentActivity) { +abstract class GitOperation(protected val callingActivity: FragmentActivity) { abstract val commands: Array> - private var provider: CredentialsProvider? = null private val hostKeyFile = callingActivity.filesDir.resolve(".host_key") - protected var finishFromErrorDialog = true - protected val repository = PasswordRepository.getRepository(gitDir) + private var sshSessionFactory: SshjSessionFactory? = null + + protected val repository = PasswordRepository.getRepository(null)!! protected val git = Git(repository) protected val remoteBranch = GitSettings.branch @@ -90,27 +85,6 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment } } - private fun withPasswordAuthentication(passwordFinder: InteractivePasswordFinder): GitOperation { - val sessionFactory = SshjSessionFactory(SshAuthData.Password(passwordFinder), hostKeyFile) - SshSessionFactory.setInstance(sessionFactory) - this.provider = HttpsCredentialsProvider(passwordFinder) - return this - } - - private fun withSshKeyAuthentication(passphraseFinder: InteractivePasswordFinder): GitOperation { - val sessionFactory = SshjSessionFactory(SshAuthData.SshKey(passphraseFinder), hostKeyFile) - SshSessionFactory.setInstance(sessionFactory) - this.provider = null - return this - } - - private fun withOpenKeychainAuthentication(activity: FragmentActivity): GitOperation { - val sessionFactory = SshjSessionFactory(SshAuthData.OpenKeychain(activity), hostKeyFile) - SshSessionFactory.setInstance(sessionFactory) - this.provider = null - return this - } - private fun getSshKey(make: Boolean) { try { // Ask the UserPreference to provide us with the ssh-key @@ -124,16 +98,30 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment } } - fun setCredentialProvider() { - provider?.let { credentialsProvider -> - commands.filterIsInstance>().forEach { it.setCredentialsProvider(credentialsProvider) } + private fun registerAuthProviders(authData: SshAuthData, credentialsProvider: CredentialsProvider? = null) { + sshSessionFactory = SshjSessionFactory(authData, hostKeyFile) + commands.filterIsInstance>().forEach { command -> + command.setTransportConfigCallback { transport: Transport -> + (transport as? SshTransport)?.sshSessionFactory = sshSessionFactory + credentialsProvider?.let { transport.credentialsProvider = it } + } } } /** - * Executes the GitCommand in an async task + * Executes the GitCommand in an async task. */ - abstract suspend fun execute() + suspend fun execute(): Result { + if (!preExecute()) { + return Ok(Unit) + } + val operationResult = GitCommandExecutor( + callingActivity, + this, + ).execute() + postExecute() + return operationResult + } private fun onMissingSshKeyFile() { MaterialAlertDialogBuilder(callingActivity) @@ -151,9 +139,7 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment }.show() } - suspend fun executeAfterAuthentication( - authMode: AuthMode, - ) { + suspend fun executeAfterAuthentication(authMode: AuthMode): Result { when (authMode) { AuthMode.SshKey -> if (SshKey.exists) { if (SshKey.mustAuthenticate) { @@ -167,9 +153,12 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment } when (result) { is BiometricAuthenticator.Result.Success -> { - withSshKeyAuthentication(CredentialFinder(callingActivity, authMode)).execute() + registerAuthProviders( + SshAuthData.SshKey(CredentialFinder(callingActivity, AuthMode.SshKey))) + } + is BiometricAuthenticator.Result.Cancelled -> { + return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER)) } - is BiometricAuthenticator.Result.Cancelled -> callingActivity.finish() is BiometricAuthenticator.Result.Failure -> { throw IllegalStateException("Biometric authentication failures should be ignored") } @@ -183,41 +172,36 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment } } } else { - withSshKeyAuthentication(CredentialFinder(callingActivity, authMode)).execute() + registerAuthProviders(SshAuthData.SshKey(CredentialFinder(callingActivity, AuthMode.SshKey))) } } else { onMissingSshKeyFile() } - AuthMode.OpenKeychain -> withOpenKeychainAuthentication(callingActivity).execute() - AuthMode.Password -> withPasswordAuthentication( - CredentialFinder(callingActivity, authMode)).execute() - AuthMode.None -> execute() + AuthMode.OpenKeychain -> registerAuthProviders(SshAuthData.OpenKeychain(callingActivity)) + AuthMode.Password -> { + val credentialFinder = CredentialFinder(callingActivity, AuthMode.Password) + val httpsCredentialProvider = HttpsCredentialsProvider(credentialFinder) + registerAuthProviders( + SshAuthData.Password(CredentialFinder(callingActivity, AuthMode.Password)), + httpsCredentialProvider) + } + AuthMode.None -> { + } } + return execute() } /** - * Action to execute on error + * Called before execution of the Git operation. + * Return false to cancel. */ - @CallSuper - open fun onError(err: Exception) { - // Clear various auth related fields on failure - callingActivity.getEncryptedPrefs("git_operation").edit { - remove(PreferenceKeys.HTTPS_PASSWORD) - } - callingActivity.sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) } - d(err) - MaterialAlertDialogBuilder(callingActivity) - .setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title)) - .setMessage(ErrorMessages[err]) - .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> - if (finishFromErrorDialog) callingActivity.finish() - }.show() - } + open fun preExecute() = true - /** - * Action to execute on success - */ - open fun onSuccess() {} + private suspend fun postExecute() { + withContext(Dispatchers.IO) { + sshSessionFactory?.close() + } + } companion object { diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt index d31b2aa4..d4ce6d01 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt @@ -5,24 +5,11 @@ package com.zeapo.pwdstore.git.operation import androidx.appcompat.app.AppCompatActivity -import com.zeapo.pwdstore.git.GitCommandExecutor -import java.io.File -import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.GitCommand -/** - * Creates a new git operation - * - * @param fileDir the git working tree directory - * @param callingActivity the calling activity - */ -class PullOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { +class PullOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) { override val commands: Array> = arrayOf( - Git(repository).pull().setRebase(true).setRemote("origin"), + git.pull().setRebase(true).setRemote("origin"), ) - - override suspend fun execute() { - GitCommandExecutor(callingActivity, this).execute() - } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt index 33b20a06..d3bee209 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt @@ -5,25 +5,11 @@ package com.zeapo.pwdstore.git.operation import androidx.appcompat.app.AppCompatActivity -import com.zeapo.pwdstore.git.GitCommandExecutor -import java.io.File -import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.GitCommand -/** - * Creates a new git operation - * - * @param fileDir the git working tree directory - * @param callingActivity the calling activity - */ -class PushOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { +class PushOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) { override val commands: Array> = arrayOf( - Git(repository).push().setPushAll().setRemote("origin"), + git.push().setPushAll().setRemote("origin"), ) - - override suspend fun execute() { - setCredentialProvider() - GitCommandExecutor(callingActivity, this).execute() - } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt index 0d0dc019..2df7a2b4 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt @@ -5,17 +5,9 @@ package com.zeapo.pwdstore.git.operation import androidx.appcompat.app.AppCompatActivity -import com.zeapo.pwdstore.git.GitCommandExecutor -import java.io.File import org.eclipse.jgit.api.ResetCommand -/** - * Creates a new git operation - * - * @param fileDir the git working tree directory - * @param callingActivity the calling activity - */ -class ResetToRemoteOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { +class ResetToRemoteOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) { override val commands = arrayOf( // Stage all files @@ -28,8 +20,4 @@ class ResetToRemoteOperation(fileDir: File, callingActivity: AppCompatActivity) // branches from 'master' to anything else. git.branchCreate().setName(remoteBranch).setForce(true), ) - - override suspend fun execute() { - GitCommandExecutor(callingActivity, this).execute() - } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt index 35b12810..a0a80fe3 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt @@ -5,16 +5,8 @@ package com.zeapo.pwdstore.git.operation import androidx.appcompat.app.AppCompatActivity -import com.zeapo.pwdstore.git.GitCommandExecutor -import java.io.File -/** - * Creates a new git operation - * - * @param fileDir the git working tree directory - * @param callingActivity the calling activity - */ -class SyncOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { +class SyncOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) { override val commands = arrayOf( // Stage all files @@ -28,8 +20,4 @@ class SyncOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOper // Push it all back git.push().setPushAll().setRemote("origin"), ) - - override suspend fun execute() { - GitCommandExecutor(callingActivity, this).execute() - } } diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt index b5295f17..1eb1c95d 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt @@ -77,7 +77,6 @@ class FolderCreationDialogFragment : DialogFragment() { R.string.git_commit_gpg_id, BasePgpActivity.getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) ), - finishActivityOnEnd = false, ) dismiss() } diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index f41899b9..eea69dc4 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -7,7 +7,6 @@ package com.zeapo.pwdstore.utils import android.app.KeyguardManager import android.content.ClipboardManager import android.content.Context -import android.content.Intent import android.content.SharedPreferences import android.os.Build import android.util.Base64 @@ -24,8 +23,10 @@ import androidx.preference.PreferenceManager import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import com.github.ajalt.timberkt.d +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result import com.google.android.material.snackbar.Snackbar -import com.zeapo.pwdstore.git.GitCommandExecutor +import com.zeapo.pwdstore.R import com.zeapo.pwdstore.git.operation.GitOperation import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory import java.io.File @@ -59,6 +60,7 @@ fun FragmentActivity.snackbar( length: Int = Snackbar.LENGTH_SHORT, ): Snackbar { val snackbar = Snackbar.make(view, message, length) + snackbar.anchorView = findViewById(R.id.fab) snackbar.show() return snackbar } @@ -108,17 +110,11 @@ fun SharedPreferences.getString(key: String): String? = getString(key, null) suspend fun FragmentActivity.commitChange( message: String, - finishWithResultOnEnd: Intent? = null, - finishActivityOnEnd: Boolean = true, -) { +): Result { if (!PasswordRepository.isGitRepo()) { - if (finishWithResultOnEnd != null) { - setResult(FragmentActivity.RESULT_OK, finishWithResultOnEnd) - finish() - } - return + return Ok(Unit) } - object : GitOperation(getRepositoryDirectory(), this@commitChange) { + return object : GitOperation(this@commitChange) { override val commands = arrayOf( // Stage all files git.add().addFilepattern("."), @@ -128,14 +124,9 @@ suspend fun FragmentActivity.commitChange( git.commit().setAll(true).setMessage(message), ) - override suspend fun execute() { - d { "Comitting with message: '$message'" } - GitCommandExecutor( - this@commitChange, - this, - finishWithResultOnEnd, - finishActivityOnEnd, - ).execute() + override fun preExecute(): Boolean { + d { "Committing with message: '$message'" } + return true } }.execute() } diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Result.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Result.kt deleted file mode 100644 index d152cba6..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Result.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package com.zeapo.pwdstore.utils - -/** - * Emulates the Rust Result enum but without returning a value in the [Ok] case. - * https://doc.rust-lang.org/std/result/enum.Result.html - */ -sealed class Result { - - object Ok : Result() - data class Err(val err: Exception) : Result() -} diff --git a/app/src/main/res/layout/activity_git_clone.xml b/app/src/main/res/layout/activity_git_clone.xml index b7ec6fc1..ad0a186b 100644 --- a/app/src/main/res/layout/activity_git_clone.xml +++ b/app/src/main/res/layout/activity_git_clone.xml @@ -11,7 +11,7 @@ android:background="?android:attr/windowBackground" android:padding="@dimen/activity_horizontal_margin" tools:background="@color/white" - tools:context="com.zeapo.pwdstore.git.GitOperationActivity"> + tools:context="com.zeapo.pwdstore.git.GitServerConfigActivity"> - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c8b75cd..99ff2223 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -402,6 +402,8 @@ Remote rejected non-fast-forward push. Check receive.denyNonFastForwards variable in config file of destination repository. Unknown host: %1$s Running git operation… + There was a conflict when trying to rebase. Your local %1$s branch was pushed to another branch named %2$s\n Use this branch to resolve conflict on your computer + The repository is not rebasing, no need to push to another branch OpenKeychain not installed diff --git a/build.gradle.kts b/build.gradle.kts index ac356d2b..bdb166aa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,7 +53,7 @@ subprojects { tasks.withType { kotlinOptions { jvmTarget = "1.8" - freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn") + freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn", "-Xallow-result-return-type") languageVersion = "1.4" } } diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 506a901a..c3816b7d 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -58,6 +58,7 @@ object Dependencies { const val eddsa = "net.i2p.crypto:eddsa:0.3.0" const val fastscroll = "me.zhanghai.android.fastscroll:library:1.1.4" const val jgit = "org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r" + const val kotlin_result = "com.michael-bull.kotlin-result:kotlin-result:1.1.9" const val leakcanary = "com.squareup.leakcanary:leakcanary-android:2.4" const val plumber = "com.squareup.leakcanary:plumber-android:2.4" const val sshj = "com.hierynomus:sshj:0.30.0"