Refactor Git operations and auth (#1066)

Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Fabian Henneke 2020-09-03 10:48:14 +02:00 committed by GitHub
parent 258ccc6016
commit 3840f43fa0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 276 additions and 493 deletions

View file

@ -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)

View file

@ -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.** { *; }

View file

@ -51,10 +51,6 @@
android:windowSoftInputMode="stateAlwaysHidden"
tools:node="replace" />
<activity
android:name=".git.GitOperationActivity"
android:theme="@style/NoBackgroundTheme" />
<activity
android:name=".git.GitServerConfigActivity"
android:label="@string/title_activity_git_clone"

View file

@ -40,17 +40,17 @@ class LaunchActivity : AppCompatActivity() {
}
private fun startTargetActivity(noAuth: Boolean) {
if (intent.action == ACTION_DECRYPT_PASS) {
val intentToStart = if (intent.action == ACTION_DECRYPT_PASS)
Intent(this, DecryptActivity::class.java).apply {
putExtra("NAME", intent.getStringExtra("NAME"))
putExtra("FILE_PATH", intent.getStringExtra("FILE_PATH"))
putExtra("REPO_PATH", intent.getStringExtra("REPO_PATH"))
putExtra("LAST_CHANGED_TIMESTAMP", intent.getLongExtra("LAST_CHANGED_TIMESTAMP", 0L))
startActivity(this)
}
} else {
startActivity(Intent(this, PasswordStore::class.java))
}
else
Intent(this, PasswordStore::class.java)
startActivity(intentToStart)
Handler().postDelayed({ finish() }, if (noAuth) 0L else 500L)
}

View file

@ -19,12 +19,13 @@ import androidx.appcompat.view.ActionMode
import androidx.core.content.edit
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.michaelbull.result.fold
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding
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
@ -39,6 +40,7 @@ import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import com.zeapo.pwdstore.utils.viewBinding
import java.io.File
import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder
class PasswordFragment : Fragment(R.layout.password_recycler_view) {
@ -99,9 +101,17 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
AuthMode.None -> 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,
)
}
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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,16 +332,15 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds ->
gpgIdentifierFile.writeText(keyIds.joinToString("\n"))
lifecycleScope.launch {
commitChange(
getString(
commitChange(getString(
R.string.git_commit_gpg_id,
getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
)
)
}
)).onSuccess {
encrypt(data)
}
}
}
}
}.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java))
return@with
}
@ -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) {

View file

@ -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)
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" }
setResult(RESULT_CANCELED)
return Err(IllegalArgumentException("$operation is not a valid Git operation"))
}
}
return op.executeAfterAuthentication(GitSettings.authMode)
}
fun finishOnSuccessHandler(@Suppress("UNUSED_PARAMETER") nothing: Unit) {
finish()
return
}
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()
}
}
op.executeAfterAuthentication(GitSettings.authMode)
} catch (e: Exception) {
e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).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 {

View file

@ -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()
}
}
}.also {
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
}
}
}

View file

@ -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,
)
}
}
}
/**

View file

@ -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)
}
}
}

View file

@ -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,
)
}
}
}
}

View file

@ -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) {
override fun preExecute() = 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() {
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
}
}

View file

@ -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)
}
}

View file

@ -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,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 {

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -77,7 +77,6 @@ class FolderCreationDialogFragment : DialogFragment() {
R.string.git_commit_gpg_id,
BasePgpActivity.getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
),
finishActivityOnEnd = false,
)
dismiss()
}

View file

@ -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<Unit, Throwable> {
if (!PasswordRepository.isGitRepo()) {
if (finishWithResultOnEnd != null) {
setResult(FragmentActivity.RESULT_OK, finishWithResultOnEnd)
finish()
return Ok(Unit)
}
return
}
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()
}

View file

@ -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()
}

View file

@ -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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"

View file

@ -1,15 +0,0 @@
<!--
~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
~ SPDX-License-Identifier: GPL-3.0-only
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:pwstore="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.zeapo.pwdstore.git.GitServerConfigActivity">
<item
android:id="@+id/user_pref"
android:orderInCategory="100"
android:title="@string/action_settings"
pwstore:showAsAction="never" />
</menu>

View file

@ -402,6 +402,8 @@
<string name="git_push_other_error">Remote rejected non-fast-forward push. Check receive.denyNonFastForwards variable in config file of destination repository.</string>
<string name="git_unknown_host">Unknown host: %1$s</string>
<string name="git_operation_running">Running git operation…</string>
<string name="git_break_out_of_detached_success">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</string>
<string name="git_break_out_of_detached_unneeded">The repository is not rebasing, no need to push to another branch</string>
<!-- OpenKeychain not installed -->
<string name="openkeychain_not_installed_title">OpenKeychain not installed</string>

View file

@ -53,7 +53,7 @@ subprojects {
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn", "-Xallow-result-return-type")
languageVersion = "1.4"
}
}

View file

@ -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"