Remove GitAsyncTask and replace with non-blocking coroutines (#865)
Co-authored-by: Fabian Henneke <fabian@henneke.me>
This commit is contained in:
parent
12a83e5c36
commit
14c44bf584
32 changed files with 632 additions and 650 deletions
|
@ -23,6 +23,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
if (BuildConfig.ENABLE_DEBUG_FEATURES || prefs?.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false) ==
|
||||
true) {
|
||||
|
@ -52,4 +53,9 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
|
|||
else -> MODE_NIGHT_AUTO_BATTERY
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
lateinit var instance: Application
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,6 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
|
|||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
|
|
@ -34,7 +34,6 @@ import androidx.fragment.app.FragmentManager
|
|||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.github.ajalt.timberkt.d
|
||||
import com.github.ajalt.timberkt.e
|
||||
|
@ -581,7 +580,12 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
|||
intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath)
|
||||
registerForActivityResult(StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
commitChange(resources.getString(R.string.git_commit_add_text, result.data?.extras?.getString("LONG_NAME")))
|
||||
lifecycleScope.launch {
|
||||
commitChange(
|
||||
resources.getString(R.string.git_commit_add_text, result.data?.extras?.getString("LONG_NAME")),
|
||||
finishActivityOnEnd = false,
|
||||
)
|
||||
}
|
||||
refreshPasswordList()
|
||||
}
|
||||
}.launch(intent)
|
||||
|
@ -618,11 +622,15 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
|||
selectedItems.map { item -> item.file.deleteRecursively() }
|
||||
refreshPasswordList()
|
||||
AutofillMatcher.updateMatches(applicationContext, delete = filesToDelete)
|
||||
commitChange(resources.getString(R.string.git_commit_remove_text,
|
||||
selectedItems.joinToString(separator = ", ") { item ->
|
||||
item.file.toRelativeString(getRepositoryDirectory(this))
|
||||
}
|
||||
))
|
||||
val fmt = selectedItems.joinToString(separator = ", ") { item ->
|
||||
item.file.toRelativeString(getRepositoryDirectory(this@PasswordStore))
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
commitChange(
|
||||
resources.getString(R.string.git_commit_remove_text, fmt),
|
||||
finishActivityOnEnd = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(resources.getString(R.string.dialog_no), null)
|
||||
.show()
|
||||
|
@ -688,14 +696,20 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
|||
val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename)
|
||||
val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
|
||||
withContext(Dispatchers.Main) {
|
||||
commitChange(resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName))
|
||||
commitChange(
|
||||
resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName),
|
||||
finishActivityOnEnd = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val repoDir = getRepositoryDirectory(applicationContext).absolutePath
|
||||
val relativePath = getRelativePath("${target.absolutePath}/", repoDir)
|
||||
withContext(Dispatchers.Main) {
|
||||
commitChange(resources.getString(R.string.git_commit_move_multiple_text,
|
||||
getRelativePath("${target.absolutePath}/", getRepositoryDirectory(applicationContext).absolutePath)
|
||||
))
|
||||
commitChange(
|
||||
resources.getString(R.string.git_commit_move_multiple_text, relativePath),
|
||||
finishActivityOnEnd = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -746,7 +760,10 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
|||
else -> lifecycleScope.launch(Dispatchers.IO) {
|
||||
moveFile(oldCategory.file, newCategory)
|
||||
withContext(Dispatchers.Main) {
|
||||
commitChange(resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name))
|
||||
commitChange(
|
||||
resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name),
|
||||
finishActivityOnEnd = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import android.view.View
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding
|
||||
import com.zeapo.pwdstore.ui.adapters.PasswordItemRecyclerAdapter
|
||||
|
|
|
@ -22,7 +22,6 @@ import androidx.core.text.buildSpannedString
|
|||
import androidx.core.text.underline
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.ajalt.timberkt.e
|
||||
import com.zeapo.pwdstore.FilterMode
|
||||
|
|
|
@ -15,6 +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
|
||||
|
@ -27,6 +28,7 @@ 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() {
|
||||
|
@ -144,10 +146,12 @@ class AutofillSaveActivity : AppCompatActivity() {
|
|||
}
|
||||
// PasswordCreationActivity delegates committing the added file to PasswordStore. Since
|
||||
// PasswordStore is not involved in an AutofillScenario, we have to commit the file ourselves.
|
||||
commitChange(
|
||||
getString(R.string.git_commit_add_text, longName),
|
||||
finishWithResultOnEnd = resultIntent
|
||||
)
|
||||
lifecycleScope.launch {
|
||||
commitChange(
|
||||
getString(R.string.git_commit_add_text, longName),
|
||||
finishWithResultOnEnd = resultIntent
|
||||
)
|
||||
}
|
||||
// GitAsyncTask will finish the activity for us.
|
||||
}
|
||||
}.launch(saveIntent)
|
||||
|
|
|
@ -329,12 +329,14 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
gpgIdentifierFile.writeText(keyIds.joinToString("\n"))
|
||||
val repo = PasswordRepository.getRepository(null)
|
||||
if (repo != null) {
|
||||
commitChange(
|
||||
getString(
|
||||
R.string.git_commit_gpg_id,
|
||||
getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
|
||||
lifecycleScope.launch {
|
||||
commitChange(
|
||||
getString(
|
||||
R.string.git_commit_gpg_id,
|
||||
getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
encrypt(data)
|
||||
}
|
||||
|
@ -422,7 +424,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
AutofillPreferences.directoryStructure(applicationContext)
|
||||
val entry = PasswordEntry(content)
|
||||
returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password)
|
||||
val username = entry.username ?: directoryStructure.getUsernameFor(file)
|
||||
val username = entry.username
|
||||
?: directoryStructure.getUsernameFor(file)
|
||||
returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
|
||||
}
|
||||
|
||||
|
@ -430,12 +433,14 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
if (repo != null) {
|
||||
val status = Git(repo).status().call()
|
||||
if (status.modified.isNotEmpty()) {
|
||||
commitChange(
|
||||
getString(
|
||||
R.string.git_commit_edit_text,
|
||||
getLongName(fullPath, repoPath, editName)
|
||||
lifecycleScope.launch {
|
||||
commitChange(
|
||||
getString(
|
||||
R.string.git_commit_edit_text,
|
||||
getLongName(fullPath, repoPath, editName)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.annotation.CallSuper
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.github.ajalt.timberkt.Timber.tag
|
||||
import com.github.ajalt.timberkt.e
|
||||
|
@ -20,11 +21,19 @@ import com.zeapo.pwdstore.R
|
|||
import com.zeapo.pwdstore.git.config.ConnectionMode
|
||||
import com.zeapo.pwdstore.git.config.Protocol
|
||||
import com.zeapo.pwdstore.git.config.SshApiSessionFactory
|
||||
import com.zeapo.pwdstore.git.operation.BreakOutOfDetached
|
||||
import com.zeapo.pwdstore.git.operation.CloneOperation
|
||||
import com.zeapo.pwdstore.git.operation.GitOperation
|
||||
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 java.io.File
|
||||
import java.net.URI
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Abstract AppCompatActivity that holds some information that is commonly shared across git-related
|
||||
|
@ -166,7 +175,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
|
|||
*
|
||||
* @param operation The type of git operation to launch
|
||||
*/
|
||||
fun launchGitOperation(operation: Int) {
|
||||
suspend fun launchGitOperation(operation: Int) {
|
||||
if (url == null) {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
|
@ -190,12 +199,12 @@ abstract class BaseGitActivity : AppCompatActivity() {
|
|||
|
||||
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(this))
|
||||
val op = when (operation) {
|
||||
REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, this).setCommand(url!!)
|
||||
REQUEST_PULL -> PullOperation(localDir, this).setCommand()
|
||||
REQUEST_PUSH -> PushOperation(localDir, this).setCommand()
|
||||
REQUEST_SYNC -> SyncOperation(localDir, this).setCommands()
|
||||
BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(localDir, this).setCommands()
|
||||
REQUEST_RESET -> ResetToRemoteOperation(localDir, this).setCommands()
|
||||
REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, 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)
|
||||
SshApiSessionFactory.POST_SIGNATURE -> return
|
||||
else -> {
|
||||
tag(TAG).e { "Operation not recognized : $operation" }
|
||||
|
@ -239,7 +248,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
|
|||
if (identityBuilder != null) {
|
||||
identityBuilder!!.consume(data)
|
||||
}
|
||||
launchGitOperation(requestCode)
|
||||
lifecycleScope.launch { launchGitOperation(requestCode) }
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
|
|
@ -1,90 +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 androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.zeapo.pwdstore.R
|
||||
import com.zeapo.pwdstore.utils.PreferenceKeys
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.api.GitCommand
|
||||
import org.eclipse.jgit.api.PushCommand
|
||||
import org.eclipse.jgit.api.RebaseCommand
|
||||
|
||||
class BreakOutOfDetached(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) {
|
||||
|
||||
private lateinit var commands: List<GitCommand<out Any>>
|
||||
private val gitBranch = PreferenceManager
|
||||
.getDefaultSharedPreferences(callingActivity.applicationContext)
|
||||
.getString(PreferenceKeys.GIT_BRANCH_NAME, "master")
|
||||
|
||||
/**
|
||||
* Sets the command
|
||||
*
|
||||
* @return the current object
|
||||
*/
|
||||
fun setCommands(): BreakOutOfDetached {
|
||||
val git = Git(repository)
|
||||
val branchName = "conflicting-$gitBranch-${System.currentTimeMillis()}"
|
||||
|
||||
this.commands = listOf(
|
||||
// abort the rebase
|
||||
git.rebase().setOperation(RebaseCommand.Operation.ABORT),
|
||||
// git checkout -b conflict-branch
|
||||
git.checkout().setCreateBranch(true).setName(branchName),
|
||||
// push the changes
|
||||
git.push().setRemote("origin"),
|
||||
// switch back to ${gitBranch}
|
||||
git.checkout().setName(gitBranch)
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
val git = Git(repository)
|
||||
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
|
||||
}
|
||||
|
||||
if (this.provider != null) {
|
||||
// set the credentials for push command
|
||||
this.commands.forEach { cmd ->
|
||||
if (cmd is PushCommand) {
|
||||
cmd.setCredentialsProvider(this.provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
GitAsyncTask(callingActivity, this, null)
|
||||
.execute(*this.commands.toTypedArray())
|
||||
}
|
||||
|
||||
override fun onError(err: Exception) {
|
||||
MaterialAlertDialogBuilder(callingActivity)
|
||||
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
|
||||
.setMessage("Error occurred when checking out another branch operation ${err.message}")
|
||||
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
|
||||
callingActivity.finish()
|
||||
}.show()
|
||||
}
|
||||
|
||||
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 $gitBranch branch was pushed to another branch named conflicting-$gitBranch-....\n" +
|
||||
"Use this branch to resolve conflict on your computer")
|
||||
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
|
||||
callingActivity.finish()
|
||||
}.show()
|
||||
}
|
||||
}
|
|
@ -1,53 +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 androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.zeapo.pwdstore.R
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.CloneCommand
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
/**
|
||||
* Creates a new clone operation
|
||||
*
|
||||
* @param fileDir the git working tree directory
|
||||
* @param callingActivity the calling activity
|
||||
*/
|
||||
class CloneOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) {
|
||||
|
||||
/**
|
||||
* Sets the command using the repository uri
|
||||
*
|
||||
* @param uri the uri of the repository
|
||||
* @return the current object
|
||||
*/
|
||||
fun setCommand(uri: String): CloneOperation {
|
||||
this.command = Git.cloneRepository()
|
||||
.setCloneAllBranches(true)
|
||||
.setDirectory(repository?.workTree)
|
||||
.setURI(uri)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
(this.command as? CloneCommand)?.setCredentialsProvider(this.provider)
|
||||
GitAsyncTask(callingActivity, this, Intent()).execute(this.command)
|
||||
}
|
||||
|
||||
override fun onError(err: Exception) {
|
||||
super.onError(err)
|
||||
MaterialAlertDialogBuilder(callingActivity)
|
||||
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
|
||||
.setMessage("Error occurred during the clone operation, " +
|
||||
callingActivity.resources.getString(R.string.jgit_error_dialog_text) +
|
||||
err.message +
|
||||
"\nPlease check the FAQ for possible reasons why this error might occur.")
|
||||
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
75
app/src/main/java/com/zeapo/pwdstore/git/ErrorMessages.kt
Normal file
75
app/src/main/java/com/zeapo/pwdstore/git/ErrorMessages.kt
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.os.RemoteException
|
||||
import com.zeapo.pwdstore.Application
|
||||
import com.zeapo.pwdstore.R
|
||||
|
||||
/**
|
||||
* Supertype for all Git-related [Exception]s that can be thrown by [GitCommandExecutor.execute].
|
||||
*/
|
||||
sealed class GitException(message: String? = null) : Exception(message) {
|
||||
|
||||
/**
|
||||
* Encapsulates possible errors from a [org.eclipse.jgit.api.PullCommand].
|
||||
*/
|
||||
class PullException(val reason: Reason) : GitException() {
|
||||
|
||||
enum class Reason {
|
||||
REBASE_FAILED,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates possible errors from a [org.eclipse.jgit.api.PushCommand].
|
||||
*/
|
||||
class PushException(val reason: Reason, vararg val fmt: String) : GitException() {
|
||||
enum class Reason {
|
||||
NON_FAST_FORWARD,
|
||||
REMOTE_REJECTED,
|
||||
GENERIC,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ErrorMessages {
|
||||
|
||||
private val PULL_REASON_MAP = mapOf(
|
||||
GitException.PullException.Reason.REBASE_FAILED to R.string.git_pull_fail_error,
|
||||
)
|
||||
|
||||
private val PUSH_REASON_MAP = mapOf(
|
||||
GitException.PushException.Reason.NON_FAST_FORWARD to R.string.git_push_nff_error,
|
||||
GitException.PushException.Reason.REMOTE_REJECTED to R.string.git_push_other_error,
|
||||
GitException.PushException.Reason.GENERIC to R.string.git_push_generic_error,
|
||||
)
|
||||
|
||||
operator fun get(throwable: Throwable?): String {
|
||||
val resources = Application.instance.resources
|
||||
if (throwable == null) return resources.getString(R.string.git_unknown_error)
|
||||
return when (val rootCause = rootCause(throwable)) {
|
||||
is GitException.PullException -> {
|
||||
resources.getString(PULL_REASON_MAP.getValue(rootCause.reason))
|
||||
}
|
||||
is GitException.PushException -> {
|
||||
resources.getString(PUSH_REASON_MAP.getValue(rootCause.reason), *rootCause.fmt)
|
||||
}
|
||||
else -> throwable.message ?: resources.getString(R.string.git_unknown_error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun rootCause(throwable: Throwable): Throwable {
|
||||
var cause = throwable
|
||||
while (cause.cause != null) {
|
||||
if (cause is GitException) break
|
||||
val nextCause = cause.cause!!
|
||||
if (nextCause is RemoteException) break
|
||||
cause = nextCause
|
||||
}
|
||||
return cause
|
||||
}
|
||||
}
|
|
@ -2,23 +2,27 @@
|
|||
* 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.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.AsyncTask
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.SshjSessionFactory
|
||||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
import com.zeapo.pwdstore.git.operation.GitOperation
|
||||
import com.zeapo.pwdstore.utils.Result
|
||||
import com.zeapo.pwdstore.utils.snackbar
|
||||
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.GitCommand
|
||||
import org.eclipse.jgit.api.PullCommand
|
||||
import org.eclipse.jgit.api.PushCommand
|
||||
import org.eclipse.jgit.api.RebaseResult
|
||||
|
@ -26,95 +30,122 @@ import org.eclipse.jgit.api.StatusCommand
|
|||
import org.eclipse.jgit.transport.RemoteRefUpdate
|
||||
import org.eclipse.jgit.transport.SshSessionFactory
|
||||
|
||||
|
||||
class GitAsyncTask(
|
||||
activity: AppCompatActivity,
|
||||
class GitCommandExecutor(
|
||||
private val activity: FragmentActivity,
|
||||
private val operation: GitOperation,
|
||||
private val finishWithResultOnEnd: Intent?,
|
||||
private val silentlyExecute: Boolean = false
|
||||
) : AsyncTask<GitCommand<*>, Int, GitAsyncTask.Result>() {
|
||||
private val finishWithResultOnEnd: Intent? = Intent(),
|
||||
private val finishActivityOnEnd: Boolean = true,
|
||||
) {
|
||||
|
||||
private val activityWeakReference: WeakReference<AppCompatActivity> = WeakReference(activity)
|
||||
private val activity: AppCompatActivity?
|
||||
get() = activityWeakReference.get()
|
||||
private val context: Context = activity.applicationContext
|
||||
private val dialog = ProgressDialog(activity)
|
||||
|
||||
sealed class Result {
|
||||
object Ok : Result()
|
||||
data class Err(val err: Exception) : Result()
|
||||
}
|
||||
|
||||
override fun onPreExecute() {
|
||||
if (silentlyExecute) return
|
||||
dialog.run {
|
||||
setMessage(activity!!.resources.getString(R.string.running_dialog_text))
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg commands: GitCommand<*>): Result? {
|
||||
var nbChanges: Int? = null
|
||||
for (command in commands) {
|
||||
suspend fun execute() {
|
||||
operation.setCredentialProvider()
|
||||
val snackbar = activity.snackbar(
|
||||
message = activity.resources.getString(R.string.git_operation_running),
|
||||
length = Snackbar.LENGTH_INDEFINITE,
|
||||
)
|
||||
var nbChanges = 0
|
||||
var operationResult: Result = Result.Ok
|
||||
for (command in operation.commands) {
|
||||
try {
|
||||
when (command) {
|
||||
is StatusCommand -> {
|
||||
// in case we have changes, we want to keep track of it
|
||||
val status = command.call()
|
||||
val status = withContext(Dispatchers.IO) {
|
||||
command.call()
|
||||
}
|
||||
nbChanges = status.changed.size + status.missing.size
|
||||
}
|
||||
is CommitCommand -> {
|
||||
// the previous status will eventually be used to avoid a commit
|
||||
if (nbChanges == null || nbChanges > 0) command.call()
|
||||
withContext(Dispatchers.IO) {
|
||||
if (nbChanges > 0) command.call()
|
||||
}
|
||||
}
|
||||
is PullCommand -> {
|
||||
val result = command.call()
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
command.call()
|
||||
}
|
||||
val rr = result.rebaseResult
|
||||
if (rr.status === RebaseResult.Status.STOPPED) {
|
||||
return Result.Err(IOException(context.getString(R.string
|
||||
.git_pull_fail_error)))
|
||||
operationResult = Result.Err(PullException(PullException.Reason.REBASE_FAILED))
|
||||
}
|
||||
}
|
||||
is PushCommand -> {
|
||||
for (result in command.call()) {
|
||||
val results = withContext(Dispatchers.IO) {
|
||||
command.call()
|
||||
}
|
||||
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 ->
|
||||
context.getString(R.string.git_push_nff_error)
|
||||
RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> {
|
||||
PushException(PushException.Reason.NON_FAST_FORWARD)
|
||||
}
|
||||
RemoteRefUpdate.Status.REJECTED_NODELETE,
|
||||
RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED,
|
||||
RemoteRefUpdate.Status.NON_EXISTING,
|
||||
RemoteRefUpdate.Status.NOT_ATTEMPTED
|
||||
->
|
||||
(activity!!.getString(R.string.git_push_generic_error) + rru.status.name)
|
||||
RemoteRefUpdate.Status.NOT_ATTEMPTED,
|
||||
-> {
|
||||
PushException(PushException.Reason.GENERIC, rru.status.name)
|
||||
}
|
||||
RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> {
|
||||
if
|
||||
("non-fast-forward" == rru.message) {
|
||||
context.getString(R.string.git_push_other_error)
|
||||
if ("non-fast-forward" == rru.message) {
|
||||
PushException(PushException.Reason.REMOTE_REJECTED)
|
||||
} else {
|
||||
(context.getString(R.string.git_push_generic_error)
|
||||
+ rru.message)
|
||||
PushException(PushException.Reason.GENERIC, rru.message)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
|
||||
}
|
||||
if (error != null)
|
||||
Result.Err(IOException(error))
|
||||
if (error != null) {
|
||||
operationResult = Result.Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
command.call()
|
||||
withContext(Dispatchers.IO) {
|
||||
command.call()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return Result.Err(e)
|
||||
operationResult = Result.Err(e)
|
||||
}
|
||||
}
|
||||
return Result.Ok
|
||||
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()
|
||||
(SshSessionFactory.getInstance() as? SshjSessionFactory)?.clearCredentials()
|
||||
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 {
|
||||
|
@ -130,47 +161,4 @@ class GitAsyncTask(
|
|||
}
|
||||
return rootCause
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
override fun onPostExecute(maybeResult: Result?) {
|
||||
if (!silentlyExecute) dialog.dismiss()
|
||||
when (val result = maybeResult ?: Result.Err(IOException("Unexpected error"))) {
|
||||
is Result.Err -> {
|
||||
if (isExplicitlyUserInitiatedError(result.err)) {
|
||||
// Currently, this is only executed when the user cancels a password prompt
|
||||
// during authentication.
|
||||
if (finishWithResultOnEnd != null) {
|
||||
activity?.setResult(AppCompatActivity.RESULT_CANCELED)
|
||||
activity?.finish()
|
||||
}
|
||||
} else {
|
||||
e(result.err)
|
||||
operation.onError(rootCauseException(result.err))
|
||||
if (finishWithResultOnEnd != null) {
|
||||
activity?.setResult(AppCompatActivity.RESULT_CANCELED)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Result.Ok -> {
|
||||
operation.onSuccess()
|
||||
if (finishWithResultOnEnd != null) {
|
||||
activity?.setResult(AppCompatActivity.RESULT_OK, finishWithResultOnEnd)
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
(SshSessionFactory.getInstance() as? SshjSessionFactory)?.clearCredentials()
|
||||
SshSessionFactory.setInstance(null)
|
||||
}
|
||||
|
||||
}
|
|
@ -9,6 +9,7 @@ import android.os.Handler
|
|||
import android.util.Patterns
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.zeapo.pwdstore.R
|
||||
|
@ -16,6 +17,7 @@ import com.zeapo.pwdstore.databinding.ActivityGitConfigBinding
|
|||
import com.zeapo.pwdstore.utils.PasswordRepository
|
||||
import com.zeapo.pwdstore.utils.PreferenceKeys
|
||||
import com.zeapo.pwdstore.utils.viewBinding
|
||||
import kotlinx.coroutines.launch
|
||||
import org.eclipse.jgit.lib.Constants
|
||||
|
||||
class GitConfigActivity : BaseGitActivity() {
|
||||
|
@ -47,8 +49,8 @@ class GitConfigActivity : BaseGitActivity() {
|
|||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
binding.gitAbortRebase.setOnClickListener { launchGitOperation(BREAK_OUT_OF_DETACHED) }
|
||||
binding.gitResetToRemote.setOnClickListener { launchGitOperation(REQUEST_RESET) }
|
||||
binding.gitAbortRebase.setOnClickListener { lifecycleScope.launch { launchGitOperation(BREAK_OUT_OF_DETACHED) } }
|
||||
binding.gitResetToRemote.setOnClickListener { lifecycleScope.launch { launchGitOperation(REQUEST_RESET) } }
|
||||
binding.saveButton.setOnClickListener {
|
||||
val email = binding.gitUserEmail.text.toString().trim()
|
||||
val name = binding.gitUserName.text.toString().trim()
|
||||
|
|
|
@ -8,19 +8,21 @@ 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.utils.PasswordRepository
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class GitOperationActivity : BaseGitActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
when (intent.extras?.getInt(REQUEST_ARG_OP)) {
|
||||
REQUEST_PULL -> syncRepository(REQUEST_PULL)
|
||||
REQUEST_PUSH -> syncRepository(REQUEST_PUSH)
|
||||
REQUEST_SYNC -> syncRepository(REQUEST_SYNC)
|
||||
REQUEST_PULL -> lifecycleScope.launch { syncRepository(REQUEST_PULL) }
|
||||
REQUEST_PUSH -> lifecycleScope.launch { syncRepository(REQUEST_PUSH) }
|
||||
REQUEST_SYNC -> lifecycleScope.launch { syncRepository(REQUEST_SYNC) }
|
||||
else -> {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
|
@ -54,7 +56,7 @@ open class GitOperationActivity : BaseGitActivity() {
|
|||
*
|
||||
* @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH
|
||||
*/
|
||||
private fun syncRepository(operation: Int) {
|
||||
private suspend fun syncRepository(operation: Int) {
|
||||
if (serverUser.isEmpty() || serverHostname.isEmpty() || url.isNullOrEmpty())
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(getString(R.string.set_information_dialog_text))
|
||||
|
|
|
@ -10,6 +10,7 @@ import android.view.View
|
|||
import androidx.core.content.edit
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.zeapo.pwdstore.R
|
||||
|
@ -20,6 +21,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository
|
|||
import com.zeapo.pwdstore.utils.PreferenceKeys
|
||||
import com.zeapo.pwdstore.utils.viewBinding
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Activity that encompasses both the initial clone as well as editing the server config for future
|
||||
|
@ -171,7 +173,7 @@ class GitServerConfigActivity : BaseGitActivity() {
|
|||
.setPositiveButton(R.string.dialog_delete) { dialog, _ ->
|
||||
try {
|
||||
localDir.deleteRecursively()
|
||||
launchGitOperation(REQUEST_CLONE)
|
||||
lifecycleScope.launch { launchGitOperation(REQUEST_CLONE) }
|
||||
} catch (e: IOException) {
|
||||
// TODO Handle the exception correctly if we are unable to delete the directory...
|
||||
e.printStackTrace()
|
||||
|
@ -201,7 +203,7 @@ class GitServerConfigActivity : BaseGitActivity() {
|
|||
e.printStackTrace()
|
||||
MaterialAlertDialogBuilder(this).setMessage(e.message).show()
|
||||
}
|
||||
launchGitOperation(REQUEST_CLONE)
|
||||
lifecycleScope.launch { launchGitOperation(REQUEST_CLONE) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +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 androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.zeapo.pwdstore.R
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.api.PullCommand
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
/**
|
||||
* Sets the command
|
||||
*
|
||||
* @return the current object
|
||||
*/
|
||||
fun setCommand(): PullOperation {
|
||||
this.command = Git(repository)
|
||||
.pull()
|
||||
.setRebase(true)
|
||||
.setRemote("origin")
|
||||
return this
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
(this.command as? PullCommand)?.setCredentialsProvider(this.provider)
|
||||
GitAsyncTask(callingActivity, this, Intent()).execute(this.command)
|
||||
}
|
||||
|
||||
override fun onError(err: Exception) {
|
||||
super.onError(err)
|
||||
MaterialAlertDialogBuilder(callingActivity)
|
||||
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
|
||||
.setMessage("Error occurred during the pull operation, " +
|
||||
callingActivity.resources.getString(R.string.jgit_error_dialog_text) +
|
||||
err.message +
|
||||
"\nPlease check the FAQ for possible reasons why this error might occur.")
|
||||
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() }
|
||||
.show()
|
||||
}
|
||||
}
|
|
@ -1,50 +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 androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.zeapo.pwdstore.R
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.api.PushCommand
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
/**
|
||||
* Sets the command
|
||||
*
|
||||
* @return the current object
|
||||
*/
|
||||
fun setCommand(): PushOperation {
|
||||
this.command = Git(repository)
|
||||
.push()
|
||||
.setPushAll()
|
||||
.setRemote("origin")
|
||||
return this
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
(this.command as? PushCommand)?.setCredentialsProvider(this.provider)
|
||||
GitAsyncTask(callingActivity, this, Intent()).execute(this.command)
|
||||
}
|
||||
|
||||
override fun onError(err: Exception) {
|
||||
// TODO handle the "Nothing to push" case
|
||||
super.onError(err)
|
||||
MaterialAlertDialogBuilder(callingActivity)
|
||||
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
|
||||
.setMessage(callingActivity.getString(R.string.jgit_error_push_dialog_text) + err.message)
|
||||
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() }
|
||||
.show()
|
||||
}
|
||||
}
|
|
@ -1,69 +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 androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.zeapo.pwdstore.R
|
||||
import com.zeapo.pwdstore.utils.PreferenceKeys
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.api.GitCommand
|
||||
import org.eclipse.jgit.api.ResetCommand
|
||||
import org.eclipse.jgit.api.TransportCommand
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
private lateinit var commands: List<GitCommand<out Any>>
|
||||
|
||||
/**
|
||||
* Sets the command
|
||||
*
|
||||
* @return the current object
|
||||
*/
|
||||
fun setCommands(): ResetToRemoteOperation {
|
||||
val remoteBranch = PreferenceManager
|
||||
.getDefaultSharedPreferences(callingActivity.applicationContext)
|
||||
.getString(PreferenceKeys.GIT_BRANCH_NAME, "master")
|
||||
val git = Git(repository)
|
||||
val cmds = arrayListOf(
|
||||
git.add().addFilepattern("."),
|
||||
git.fetch().setRemote("origin"),
|
||||
git.reset().setRef("origin/$remoteBranch").setMode(ResetCommand.ResetType.HARD)
|
||||
)
|
||||
if (git.branchList().call().none { it.name == remoteBranch }) {
|
||||
cmds.add(
|
||||
git.branchCreate().setName(remoteBranch).setForce(true)
|
||||
)
|
||||
}
|
||||
commands = cmds
|
||||
return this
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
commands.filterIsInstance<TransportCommand<*, *>>().map { it.setCredentialsProvider(provider) }
|
||||
GitAsyncTask(callingActivity, this, Intent()).execute(*commands.toTypedArray())
|
||||
}
|
||||
|
||||
override fun onError(err: Exception) {
|
||||
super.onError(err)
|
||||
MaterialAlertDialogBuilder(callingActivity)
|
||||
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
|
||||
.setMessage("Error occurred during the sync operation, " +
|
||||
"\nPlease check the FAQ for possible reasons why this error might occur." +
|
||||
callingActivity.resources.getString(R.string.jgit_error_dialog_text) +
|
||||
err)
|
||||
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
|
@ -1,67 +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 androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.zeapo.pwdstore.R
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.AddCommand
|
||||
import org.eclipse.jgit.api.CommitCommand
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.api.PullCommand
|
||||
import org.eclipse.jgit.api.PushCommand
|
||||
import org.eclipse.jgit.api.StatusCommand
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
private var addCommand: AddCommand? = null
|
||||
private var statusCommand: StatusCommand? = null
|
||||
private var commitCommand: CommitCommand? = null
|
||||
private var pullCommand: PullCommand? = null
|
||||
private var pushCommand: PushCommand? = null
|
||||
|
||||
/**
|
||||
* Sets the command
|
||||
*
|
||||
* @return the current object
|
||||
*/
|
||||
fun setCommands(): SyncOperation {
|
||||
val git = Git(repository)
|
||||
this.addCommand = git.add().addFilepattern(".")
|
||||
this.statusCommand = git.status()
|
||||
this.commitCommand = git.commit().setAll(true).setMessage("[Android Password Store] Sync")
|
||||
this.pullCommand = git.pull().setRebase(true).setRemote("origin")
|
||||
this.pushCommand = git.push().setPushAll().setRemote("origin")
|
||||
return this
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
if (this.provider != null) {
|
||||
this.pullCommand?.setCredentialsProvider(this.provider)
|
||||
this.pushCommand?.setCredentialsProvider(this.provider)
|
||||
}
|
||||
GitAsyncTask(callingActivity, this, Intent()).execute(this.addCommand, this.statusCommand, this.commitCommand, this.pullCommand, this.pushCommand)
|
||||
}
|
||||
|
||||
override fun onError(err: Exception) {
|
||||
super.onError(err)
|
||||
MaterialAlertDialogBuilder(callingActivity)
|
||||
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
|
||||
.setMessage("Error occurred during the sync operation, " +
|
||||
"\nPlease check the FAQ for possible reasons why this error might occur." +
|
||||
callingActivity.resources.getString(R.string.jgit_error_dialog_text) +
|
||||
err)
|
||||
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() }
|
||||
.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
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()}"
|
||||
|
||||
override val commands = arrayOf(
|
||||
// abort the rebase
|
||||
git.rebase().setOperation(RebaseCommand.Operation.ABORT),
|
||||
// git checkout -b conflict-branch
|
||||
git.checkout().setCreateBranch(true).setName(branchName),
|
||||
// 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() {
|
||||
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")
|
||||
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
|
||||
callingActivity.finish()
|
||||
}.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
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) {
|
||||
|
||||
override val commands: Array<GitCommand<out Any>> = arrayOf(
|
||||
Git.cloneRepository().setBranch(remoteBranch).setDirectory(repository?.workTree).setURI(uri),
|
||||
)
|
||||
|
||||
override suspend fun execute() {
|
||||
GitCommandExecutor(callingActivity, this).execute()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package com.zeapo.pwdstore.git.operation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.android.material.checkbox.MaterialCheckBox
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.zeapo.pwdstore.R
|
||||
import com.zeapo.pwdstore.git.config.ConnectionMode
|
||||
import com.zeapo.pwdstore.git.config.InteractivePasswordFinder
|
||||
import com.zeapo.pwdstore.utils.PreferenceKeys
|
||||
import com.zeapo.pwdstore.utils.getEncryptedPrefs
|
||||
import com.zeapo.pwdstore.utils.requestInputFocusOnView
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class CredentialFinder(
|
||||
val callingActivity: FragmentActivity,
|
||||
val connectionMode: ConnectionMode
|
||||
) : InteractivePasswordFinder() {
|
||||
|
||||
override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) {
|
||||
val gitOperationPrefs = callingActivity.getEncryptedPrefs("git_operation")
|
||||
val credentialPref: String
|
||||
@StringRes val messageRes: Int
|
||||
@StringRes val hintRes: Int
|
||||
@StringRes val rememberRes: Int
|
||||
@StringRes val errorRes: Int
|
||||
when (connectionMode) {
|
||||
ConnectionMode.SshKey -> {
|
||||
credentialPref = PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE
|
||||
messageRes = R.string.passphrase_dialog_text
|
||||
hintRes = R.string.ssh_keygen_passphrase
|
||||
rememberRes = R.string.git_operation_remember_passphrase
|
||||
errorRes = R.string.git_operation_wrong_passphrase
|
||||
}
|
||||
ConnectionMode.Password -> {
|
||||
// Could be either an SSH or an HTTPS password
|
||||
credentialPref = PreferenceKeys.HTTPS_PASSWORD
|
||||
messageRes = R.string.password_dialog_text
|
||||
hintRes = R.string.git_operation_hint_password
|
||||
rememberRes = R.string.git_operation_remember_password
|
||||
errorRes = R.string.git_operation_wrong_password
|
||||
}
|
||||
else -> throw IllegalStateException("Only SshKey and Password connection mode ask for passwords")
|
||||
}
|
||||
val storedCredential = gitOperationPrefs.getString(credentialPref, null)
|
||||
if (isRetry)
|
||||
gitOperationPrefs.edit { remove(credentialPref) }
|
||||
if (storedCredential == null) {
|
||||
val layoutInflater = LayoutInflater.from(callingActivity)
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null)
|
||||
val credentialLayout = dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout)
|
||||
val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential)
|
||||
editCredential.setHint(hintRes)
|
||||
val rememberCredential = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential)
|
||||
rememberCredential.setText(rememberRes)
|
||||
if (isRetry)
|
||||
credentialLayout.error = callingActivity.resources.getString(errorRes)
|
||||
MaterialAlertDialogBuilder(callingActivity).run {
|
||||
setTitle(R.string.passphrase_dialog_title)
|
||||
setMessage(messageRes)
|
||||
setView(dialogView)
|
||||
setPositiveButton(R.string.dialog_ok) { _, _ ->
|
||||
val credential = editCredential.text.toString()
|
||||
if (rememberCredential.isChecked) {
|
||||
gitOperationPrefs.edit {
|
||||
putString(credentialPref, credential)
|
||||
}
|
||||
}
|
||||
cont.resume(credential)
|
||||
}
|
||||
setNegativeButton(R.string.dialog_cancel) { _, _ ->
|
||||
cont.resume(null)
|
||||
}
|
||||
setOnCancelListener {
|
||||
cont.resume(null)
|
||||
}
|
||||
create()
|
||||
}.run {
|
||||
requestInputFocusOnView<TextInputEditText>(R.id.git_auth_credential)
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
cont.resume(storedCredential)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,21 +2,18 @@
|
|||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package com.zeapo.pwdstore.git
|
||||
package com.zeapo.pwdstore.git.operation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.checkbox.MaterialCheckBox
|
||||
import com.github.ajalt.timberkt.Timber.d
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.zeapo.pwdstore.R
|
||||
import com.zeapo.pwdstore.UserPreference
|
||||
import com.zeapo.pwdstore.git.ErrorMessages
|
||||
import com.zeapo.pwdstore.git.config.ConnectionMode
|
||||
import com.zeapo.pwdstore.git.config.InteractivePasswordFinder
|
||||
import com.zeapo.pwdstore.git.config.SshApiSessionFactory
|
||||
|
@ -25,108 +22,34 @@ import com.zeapo.pwdstore.git.config.SshjSessionFactory
|
|||
import com.zeapo.pwdstore.utils.PasswordRepository
|
||||
import com.zeapo.pwdstore.utils.PreferenceKeys
|
||||
import com.zeapo.pwdstore.utils.getEncryptedPrefs
|
||||
import com.zeapo.pwdstore.utils.requestInputFocusOnView
|
||||
import java.io.File
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import net.schmizz.sshj.userauth.password.PasswordFinder
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.api.GitCommand
|
||||
import org.eclipse.jgit.api.TransportCommand
|
||||
import org.eclipse.jgit.errors.UnsupportedCredentialItem
|
||||
import org.eclipse.jgit.lib.Repository
|
||||
import org.eclipse.jgit.transport.CredentialItem
|
||||
import org.eclipse.jgit.transport.CredentialsProvider
|
||||
import org.eclipse.jgit.transport.SshSessionFactory
|
||||
import org.eclipse.jgit.transport.URIish
|
||||
|
||||
|
||||
private class GitOperationCredentialFinder(
|
||||
val callingActivity: AppCompatActivity,
|
||||
val connectionMode: ConnectionMode
|
||||
) : InteractivePasswordFinder() {
|
||||
|
||||
override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) {
|
||||
val gitOperationPrefs = callingActivity.getEncryptedPrefs("git_operation")
|
||||
val credentialPref: String
|
||||
@StringRes val messageRes: Int
|
||||
@StringRes val hintRes: Int
|
||||
@StringRes val rememberRes: Int
|
||||
@StringRes val errorRes: Int
|
||||
when (connectionMode) {
|
||||
ConnectionMode.SshKey -> {
|
||||
credentialPref = PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE
|
||||
messageRes = R.string.passphrase_dialog_text
|
||||
hintRes = R.string.ssh_keygen_passphrase
|
||||
rememberRes = R.string.git_operation_remember_passphrase
|
||||
errorRes = R.string.git_operation_wrong_passphrase
|
||||
}
|
||||
ConnectionMode.Password -> {
|
||||
// Could be either an SSH or an HTTPS password
|
||||
credentialPref = PreferenceKeys.HTTPS_PASSWORD
|
||||
messageRes = R.string.password_dialog_text
|
||||
hintRes = R.string.git_operation_hint_password
|
||||
rememberRes = R.string.git_operation_remember_password
|
||||
errorRes = R.string.git_operation_wrong_password
|
||||
}
|
||||
else -> throw IllegalStateException("Only SshKey and Password connection mode ask for passwords")
|
||||
}
|
||||
val storedCredential = gitOperationPrefs.getString(credentialPref, null)
|
||||
if (isRetry)
|
||||
gitOperationPrefs.edit { remove(credentialPref) }
|
||||
if (storedCredential == null) {
|
||||
val layoutInflater = LayoutInflater.from(callingActivity)
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null)
|
||||
val credentialLayout = dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout)
|
||||
val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential)
|
||||
editCredential.setHint(hintRes)
|
||||
val rememberCredential = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential)
|
||||
rememberCredential.setText(rememberRes)
|
||||
if (isRetry)
|
||||
credentialLayout.error = callingActivity.resources.getString(errorRes)
|
||||
MaterialAlertDialogBuilder(callingActivity).run {
|
||||
setTitle(R.string.passphrase_dialog_title)
|
||||
setMessage(messageRes)
|
||||
setView(dialogView)
|
||||
setPositiveButton(R.string.dialog_ok) { _, _ ->
|
||||
val credential = editCredential.text.toString()
|
||||
if (rememberCredential.isChecked) {
|
||||
gitOperationPrefs.edit {
|
||||
putString(credentialPref, credential)
|
||||
}
|
||||
}
|
||||
cont.resume(credential)
|
||||
}
|
||||
setNegativeButton(R.string.dialog_cancel) { _, _ ->
|
||||
cont.resume(null)
|
||||
}
|
||||
setOnCancelListener {
|
||||
cont.resume(null)
|
||||
}
|
||||
create()
|
||||
}.run {
|
||||
requestInputFocusOnView<TextInputEditText>(R.id.git_auth_credential)
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
cont.resume(storedCredential)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: AppCompatActivity) {
|
||||
abstract class GitOperation(gitDir: File, internal val callingActivity: FragmentActivity) {
|
||||
|
||||
protected val repository: Repository? = PasswordRepository.getRepository(gitDir)
|
||||
internal var provider: CredentialsProvider? = null
|
||||
internal var command: GitCommand<*>? = null
|
||||
abstract val commands: Array<GitCommand<out Any>>
|
||||
private var provider: CredentialsProvider? = null
|
||||
private val sshKeyFile = callingActivity.filesDir.resolve(".ssh_key")
|
||||
private val hostKeyFile = callingActivity.filesDir.resolve(".host_key")
|
||||
protected val repository = PasswordRepository.getRepository(gitDir)
|
||||
protected val git = Git(repository)
|
||||
protected val remoteBranch = PreferenceManager
|
||||
.getDefaultSharedPreferences(callingActivity.applicationContext)
|
||||
.getString(PreferenceKeys.GIT_BRANCH_NAME, "master")
|
||||
|
||||
private class PasswordFinderCredentialsProvider(private val username: String, private val passwordFinder: PasswordFinder) : CredentialsProvider() {
|
||||
|
||||
|
@ -181,12 +104,18 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: AppCompa
|
|||
}
|
||||
}
|
||||
|
||||
fun setCredentialProvider() {
|
||||
provider?.let { credentialsProvider ->
|
||||
commands.filterIsInstance<TransportCommand<*, *>>().forEach { it.setCredentialsProvider(credentialsProvider) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the GitCommand in an async task
|
||||
*/
|
||||
abstract fun execute()
|
||||
abstract suspend fun execute()
|
||||
|
||||
fun executeAfterAuthentication(
|
||||
suspend fun executeAfterAuthentication(
|
||||
connectionMode: ConnectionMode,
|
||||
username: String,
|
||||
identity: SshApiSessionFactory.ApiIdentity?
|
||||
|
@ -207,12 +136,12 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: AppCompa
|
|||
callingActivity.finish()
|
||||
}.show()
|
||||
} else {
|
||||
withPublicKeyAuthentication(username, GitOperationCredentialFinder(callingActivity,
|
||||
withPublicKeyAuthentication(username, CredentialFinder(callingActivity,
|
||||
connectionMode)).execute()
|
||||
}
|
||||
ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(username, identity).execute()
|
||||
ConnectionMode.Password -> withPasswordAuthentication(
|
||||
username, GitOperationCredentialFinder(callingActivity, connectionMode)).execute()
|
||||
username, CredentialFinder(callingActivity, connectionMode)).execute()
|
||||
ConnectionMode.None -> execute()
|
||||
}
|
||||
}
|
||||
|
@ -220,6 +149,7 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: AppCompa
|
|||
/**
|
||||
* Action to execute on error
|
||||
*/
|
||||
@CallSuper
|
||||
open fun onError(err: Exception) {
|
||||
// Clear various auth related fields on failure
|
||||
when (SshSessionFactory.getInstance()) {
|
||||
|
@ -236,6 +166,13 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: AppCompa
|
|||
}
|
||||
}
|
||||
}
|
||||
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)) { _, _ ->
|
||||
callingActivity.finish()
|
||||
}.show()
|
||||
}
|
||||
|
||||
/**
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
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) {
|
||||
|
||||
override val commands: Array<GitCommand<out Any>> = arrayOf(
|
||||
Git(repository).pull().setRebase(true).setRemote("origin"),
|
||||
)
|
||||
|
||||
override suspend fun execute() {
|
||||
GitCommandExecutor(callingActivity, this).execute()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
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) {
|
||||
|
||||
override val commands: Array<GitCommand<out Any>> = arrayOf(
|
||||
Git(repository).push().setPushAll().setRemote("origin"),
|
||||
)
|
||||
|
||||
override suspend fun execute() {
|
||||
setCredentialProvider()
|
||||
GitCommandExecutor(callingActivity, this).execute()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
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) {
|
||||
|
||||
override val commands = arrayOf(
|
||||
git.add().addFilepattern("."),
|
||||
git.fetch().setRemote("origin"),
|
||||
git.reset().setRef("origin/$remoteBranch").setMode(ResetCommand.ResetType.HARD),
|
||||
git.branchCreate().setName(remoteBranch).setForce(true),
|
||||
)
|
||||
|
||||
override suspend fun execute() {
|
||||
GitCommandExecutor(callingActivity, this).execute()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
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) {
|
||||
|
||||
override val commands = arrayOf(
|
||||
git.add().addFilepattern("."),
|
||||
git.status(),
|
||||
git.commit().setAll(true).setMessage("[Android Password Store] Sync"),
|
||||
git.pull().setRebase(true).setRemote("origin"),
|
||||
git.push().setPushAll().setRemote("origin"),
|
||||
)
|
||||
|
||||
override suspend fun execute() {
|
||||
GitCommandExecutor(callingActivity, this).execute()
|
||||
}
|
||||
}
|
|
@ -14,20 +14,18 @@ import android.view.View
|
|||
import android.view.autofill.AutofillManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import com.github.ajalt.timberkt.d
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.zeapo.pwdstore.git.GitAsyncTask
|
||||
import com.zeapo.pwdstore.git.GitOperation
|
||||
import com.zeapo.pwdstore.git.GitCommandExecutor
|
||||
import com.zeapo.pwdstore.git.operation.GitOperation
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
const val OPENPGP_PROVIDER = "org.sufficientlysecure.keychain"
|
||||
|
||||
|
@ -51,12 +49,14 @@ fun CharArray.clear() {
|
|||
|
||||
val Context.clipboard get() = getSystemService<ClipboardManager>()
|
||||
|
||||
fun AppCompatActivity.snackbar(
|
||||
fun FragmentActivity.snackbar(
|
||||
view: View = findViewById(android.R.id.content),
|
||||
message: String,
|
||||
length: Int = Snackbar.LENGTH_SHORT
|
||||
) {
|
||||
Snackbar.make(view, message, length).show()
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
): Snackbar {
|
||||
val snackbar = Snackbar.make(view, message, length)
|
||||
snackbar.show()
|
||||
return snackbar
|
||||
}
|
||||
|
||||
fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList()
|
||||
|
@ -97,24 +97,33 @@ fun Context.getEncryptedPrefs(fileName: String): SharedPreferences {
|
|||
)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun AppCompatActivity.commitChange(message: String, finishWithResultOnEnd: Intent? = null) {
|
||||
suspend fun FragmentActivity.commitChange(
|
||||
message: String,
|
||||
finishWithResultOnEnd: Intent? = null,
|
||||
finishActivityOnEnd: Boolean = true,
|
||||
) {
|
||||
if (!PasswordRepository.isGitRepo()) {
|
||||
if (finishWithResultOnEnd != null) {
|
||||
setResult(AppCompatActivity.RESULT_OK, finishWithResultOnEnd)
|
||||
setResult(FragmentActivity.RESULT_OK, finishWithResultOnEnd)
|
||||
finish()
|
||||
}
|
||||
return
|
||||
}
|
||||
object : GitOperation(getRepositoryDirectory(this@commitChange), this@commitChange) {
|
||||
override fun execute() {
|
||||
override val commands = arrayOf(
|
||||
git.add().addFilepattern("."),
|
||||
git.status(),
|
||||
git.commit().setAll(true).setMessage(message),
|
||||
)
|
||||
|
||||
override suspend fun execute() {
|
||||
d { "Comitting with message: '$message'" }
|
||||
val git = Git(repository)
|
||||
val task = GitAsyncTask(this@commitChange, this, finishWithResultOnEnd, silentlyExecute = true)
|
||||
task.execute(
|
||||
git.add().addFilepattern("."),
|
||||
git.commit().setAll(true).setMessage(message)
|
||||
)
|
||||
GitCommandExecutor(
|
||||
this@commitChange,
|
||||
this,
|
||||
finishWithResultOnEnd,
|
||||
finishActivityOnEnd,
|
||||
).execute()
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
@ -124,7 +133,6 @@ fun AppCompatActivity.commitChange(message: String, finishWithResultOnEnd: Inten
|
|||
* view whose id is [id]. Solution based on a StackOverflow
|
||||
* answer: https://stackoverflow.com/a/13056259/297261
|
||||
*/
|
||||
@MainThread
|
||||
fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) {
|
||||
setOnShowListener {
|
||||
findViewById<T>(id)?.apply {
|
||||
|
@ -143,6 +151,6 @@ val Context.autofillManager: AutofillManager?
|
|||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
get() = getSystemService()
|
||||
|
||||
fun AppCompatActivity.isInsideRepository(file: File): Boolean {
|
||||
fun FragmentActivity.isInsideRepository(file: File): Boolean {
|
||||
return file.canonicalPath.contains(getRepositoryDirectory(this).canonicalPath)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
|
16
app/src/main/java/com/zeapo/pwdstore/utils/Result.kt
Normal file
16
app/src/main/java/com/zeapo/pwdstore/utils/Result.kt
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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()
|
||||
}
|
|
@ -57,6 +57,7 @@ class UriTotpFinder : TotpFinder {
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val TOTP_FIELDS = arrayOf(
|
||||
"otpauth://totp",
|
||||
"totp:"
|
||||
|
|
|
@ -276,10 +276,6 @@
|
|||
<string name="autofill_ins_1_hint">Screenshot of accessibility services</string>
|
||||
<string name="autofill_ins_2_hint">Screenshot of toggle in accessibility services</string>
|
||||
<string name="autofill_ins_3_hint">Screenshot of autofill service in action</string>
|
||||
<string name="git_pull_fail_error">Pull has failed, you\'re in a detached head. Using "settings > git utils", save your changes to the remote in a new branch and resolve the conflict on your computer.</string>
|
||||
<string name="git_push_nff_error">Push was rejected by remote, run pull before pushing again. You can use Synchronize rather than pull/push as it implements both</string>
|
||||
<string name="git_push_generic_error">Push was rejected by remote, reason:</string>
|
||||
<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="jgit_error_push_dialog_text">Error occurred during the push operation:</string>
|
||||
<string name="clear_saved_passphrase_ssh">Clear saved passphrase for local SSH key</string>
|
||||
<string name="clear_saved_passphrase_https">Clear saved HTTPS password</string>
|
||||
|
@ -371,4 +367,12 @@
|
|||
<string name="short_key_ids_unsupported">A key ID in .gpg-id is too short, please use either long key IDs (16 characters) or fingerprints (40 characters)</string>
|
||||
<string name="invalid_filename_text">File name must not contain \'/\', set directory above</string>
|
||||
<string name="directory_hint">Directory</string>
|
||||
|
||||
<!-- GitException messages -->
|
||||
<string name="git_unknown_error">Unknown error</string>
|
||||
<string name="git_pull_fail_error">Pull has failed, you\'re in a detached head. Using "settings > git utils", save your changes to the remote in a new branch and resolve the conflict on your computer.</string>
|
||||
<string name="git_push_nff_error">Push was rejected by remote, run pull before pushing again. You can use Synchronize rather than pull/push as it implements both</string>
|
||||
<string name="git_push_generic_error">Push was rejected by remote, reason: %1$s</string>
|
||||
<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_operation_running">Running git operation…</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue