diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-06 10:13:20 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-06 10:13:20 +0100 |
| commit | 9ec740b0c73e33485e1d754759de46aa99444733 (patch) | |
| tree | 6be3d2e29014acc0c48a91fe86fbe8a5d671bb08 /app/src/main/java | |
| parent | c3c433a038e304ddb82b282dad2d09899daf241e (diff) | |
| download | HardenedPass-9ec740b0c73e33485e1d754759de46aa99444733.tar.gz HardenedPass-9ec740b0c73e33485e1d754759de46aa99444733.zip | |
feat(app): optionally push after each change
Changes made in the app could sit unpushed and collide with a later desktop change. The new setting, off by default, pushes after a save, move, rename or delete. It pushes rather than syncs because a rejected push changes nothing, while a pull can stop in a rebase. A failed push only shows a snackbar: the commit stays local and promptOnErrorHandler would delete saved credentials over a missing network.
Autofill saves are left out: AutofillSaveActivity closes right after saving, which would cancel the push. TODO.md tracks that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app/src/main/java')
6 files changed, 53 insertions, 5 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt b/app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt index 8e433957..617d804a 100644 --- a/app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt +++ b/app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt @@ -35,7 +35,9 @@ import app.passwordstore.R import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.ui.crypto.copyPasswordToClipboard import app.passwordstore.ui.crypto.getPGPIdentifiers +import app.passwordstore.ui.git.base.BaseGitActivity import app.passwordstore.ui.list.FolderPicker +import app.passwordstore.ui.list.commitAndPush import app.passwordstore.util.extensions.commitChange import app.passwordstore.util.extensions.getString import app.passwordstore.util.extensions.isInsideRepository @@ -148,12 +150,15 @@ fun EditorDestination( } // ponytail: commit failures are not shown, as for the list's move and delete. A commit cut // short by recreation runs again; one that already went through finds nothing to commit. - activity.commitChange( + val message = activity.getString( if (original != null) R.string.git_commit_edit_text else R.string.git_commit_add_text, saved.file.relativeTo(root).path.removeSuffix(".gpg"), ) - ) + // ponytail: AutofillSaveActivity closes right after saving, which would cancel a push, so its + // saves stay local until the next push or sync. + if (activity is BaseGitActivity) activity.commitAndPush(message) + else activity.commitChange(message) keyboard?.hide() focusManager.clearFocus() viewModel.left() diff --git a/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt b/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt index cff0964c..94a234c1 100644 --- a/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt +++ b/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt @@ -6,7 +6,9 @@ package app.passwordstore.ui.list import android.content.Context +import androidx.compose.material3.SnackbarDuration import androidx.core.content.edit +import androidx.lifecycle.lifecycleScope import app.passwordstore.R import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.ui.crypto.getLongName @@ -20,12 +22,17 @@ import app.passwordstore.util.extensions.getString import app.passwordstore.util.extensions.isInsideRepository import app.passwordstore.util.extensions.listFilesRecursively import app.passwordstore.util.extensions.sharedPrefs +import app.passwordstore.util.extensions.snackbar import app.passwordstore.util.settings.PasswordSortOrder import app.passwordstore.util.settings.PreferenceKeys +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import java.io.File import java.nio.file.Files +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import logcat.LogPriority.ERROR +import logcat.asLog import logcat.logcat /** Shared preferences file of the "recently used" sort order, keyed by base64 of the path. */ @@ -64,6 +71,27 @@ internal fun File.deleteWithoutFollowingLinks(): Boolean { return delete() } +/** + * Commits the store and, when the user asked for it, pushes without holding up the caller. A failed + * push only says so: the commit stays local, and [BaseGitActivity.promptOnErrorHandler] would + * delete the saved credentials over what may just be a missing network. + * + * ponytail: a push cut off by recreation is not resumed; the next one sends everything. An SSH key + * behind biometrics prompts on each change. + */ +suspend fun BaseGitActivity.commitAndPush(message: String) { + commitChange(message).onSuccess { + if (gitSettings.pushAfterCommit && gitSettings.url != null) { + lifecycleScope.launch { + launchGitOperation(BaseGitActivity.GitOp.PUSH).onFailure { + logcat(ERROR) { it.asLog() } + snackbar(getString(R.string.git_push_after_commit_failed), SnackbarDuration.Long) + } + } + } + } +} + /** Deletes [files] (entries or whole folders), drops their autofill matches and commits. */ suspend fun BaseGitActivity.deleteItems(files: List<File>) { val root = PasswordRepository.getRepositoryDirectory() @@ -72,7 +100,7 @@ suspend fun BaseGitActivity.deleteItems(files: List<File>) { files.forEach { it.deleteWithoutFollowingLinks() } AutofillMatcher.updateMatches(applicationContext, delete = deleted) } - commitChange( + commitAndPush( getString( R.string.git_commit_remove_text, files.joinToString(", ") { it.toRelativeString(root) }, @@ -116,14 +144,14 @@ suspend fun BaseGitActivity.moveItems(files: List<File>, target: File) { val relative = "${target.absolutePath}/".replace(repo, "").replace("/+".toRegex(), "/") getString(R.string.git_commit_move_multiple_text, relative) } - commitChange(message) + commitAndPush(message) } /** Renames the folder [from] to [to], keeps its recently-used time, then commits. */ suspend fun BaseGitActivity.renameFolder(from: File, to: File) { if (!moveFile(from, to)) return moveHistory(from, to) - commitChange(getString(R.string.git_commit_move_text, from.name, to.name)) + commitAndPush(getString(R.string.git_commit_move_text, from.name, to.name)) } /** Carries the "recently used" time of [from] over to [to]. */ diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt index ed78608f..a7bbe30a 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt @@ -258,6 +258,12 @@ fun SettingsSectionScreen( else R.string.pref_rebase_on_pull_summary ), ) + PreferenceSwitch( + title = stringResource(R.string.pref_push_after_commit_title), + checked = state.pushAfterCommit, + onCheckedChange = { onBoolean(PreferenceKeys.PUSH_AFTER_COMMIT, it) }, + summary = stringResource(R.string.pref_push_after_commit_summary), + ) if (state.isGitRepo) { PreferenceAction( title = stringResource(R.string.pref_edit_git_server_settings), diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt index ef604f1f..cfc576ae 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt @@ -55,6 +55,7 @@ data class SettingsState( val showPassword: Boolean, val copyOnDecrypt: Boolean, val rebaseOnPull: Boolean, + val pushAfterCommit: Boolean, val isGitRepo: Boolean, val httpsRemote: Boolean, val canShowSshKey: Boolean, @@ -205,6 +206,7 @@ constructor( showPassword = prefs.getBoolean(PreferenceKeys.SHOW_PASSWORD, true), copyOnDecrypt = prefs.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false), rebaseOnPull = gitSettings.rebaseOnPull, + pushAfterCommit = gitSettings.pushAfterCommit, isGitRepo = PasswordRepository.isGitRepo(), httpsRemote = gitSettings.url?.startsWith("https") == true, canShowSshKey = SshKey.canShowSshPublicKey, diff --git a/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt b/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt index 004028c4..0052f79c 100644 --- a/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt +++ b/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt @@ -125,6 +125,12 @@ constructor( settings.edit { putBoolean(PreferenceKeys.REBASE_ON_PULL, value) } } + var pushAfterCommit + get() = settings.getBoolean(PreferenceKeys.PUSH_AFTER_COMMIT, false) + set(value) { + settings.edit { putBoolean(PreferenceKeys.PUSH_AFTER_COMMIT, value) } + } + sealed class UpdateConnectionSettingsResult { class MissingUsername(val newProtocol: Protocol) : UpdateConnectionSettingsResult() diff --git a/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt b/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt index ecc7a51c..9fd2658d 100644 --- a/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt +++ b/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt @@ -86,6 +86,7 @@ object PreferenceKeys { const val PROXY_PASSWORD = "proxy_password" const val REBASE_ON_PULL = "rebase_on_pull" + const val PUSH_AFTER_COMMIT = "push_after_commit" const val DICEWARE_SEPARATOR = "diceware_separator" const val DICEWARE_LENGTH = "diceware_length" |
