aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/git
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/git')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt104
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt86
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt31
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt79
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt13
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt30
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt20
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt128
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt17
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt18
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt14
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt14
12 files changed, 200 insertions, 354 deletions
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<Unit, Throwable> {
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<Unit, Throwable> {
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)
- }
- 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
+ }.also {
+ snackbar.dismiss()
}
- 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<GitCommand<out Any>> = 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<GitCommand<out Any>>
- 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<TransportCommand<*, *>>().forEach { it.setCredentialsProvider(credentialsProvider) }
+ private fun registerAuthProviders(authData: SshAuthData, credentialsProvider: CredentialsProvider? = null) {
+ sshSessionFactory = SshjSessionFactory(authData, hostKeyFile)
+ commands.filterIsInstance<TransportCommand<*, *>>().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<Unit, Throwable> {
+ 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<Unit, Throwable> {
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,42 +172,37 @@ 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)
+ open fun preExecute() = true
+
+ private suspend fun postExecute() {
+ withContext(Dispatchers.IO) {
+ sshSessionFactory?.close()
}
- 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()
}
- /**
- * Action to execute on success
- */
- open fun onSuccess() {}
-
companion object {
const val GET_SSH_KEY_FROM_CLONE = 201
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<GitCommand<out Any>> = 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<GitCommand<out Any>> = 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()
- }
}