Present iPhone Git actions (#51)

This commit is contained in:
hugo committed 2026-08-14 17:24:41 +02:00
1 parent fcf649e56d
commit 65eb1da71c
8 files changed
+1054 -90

No files matched your search

+125 -3
View File
@@ -195,9 +195,38 @@ struct MobileAuthenticationStatus {
next_editor_id: u64,
editors: BTreeMap<u64, MobileEntryDraft>,
mutation_active: bool,
repository_operation_active: bool,
watch_shared_totp_entries: std::collections::BTreeSet<EntryPath>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum MobileRepositoryOperation {
Commit,
Fetch,
Pull,
Push,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum MobileRepositoryOperationError {
Busy,
DirtyEditor,
OpenEditor,
Unavailable,
}
pub(crate) struct MobileRepositoryOperationGuard<'a> {
authentication: &'a MobileAuthentication,
}
impl Drop for MobileRepositoryOperationGuard<'_> {
fn drop(&mut self) {
if let Ok(mut status) = self.authentication.status.lock() {
status.repository_operation_active = false;
}
}
}
/// One process-wide mobile authentication lease shared by every tab and viewer.
pub struct MobileAuthentication {
config: Config,
@@ -232,6 +261,7 @@ impl MobileAuthentication {
next_editor_id: 0,
editors: BTreeMap::new(),
mutation_active: false,
repository_operation_active: false,
watch_shared_totp_entries: config.watch_shared_totp_entries().clone(),
}),
config,
@@ -667,6 +697,7 @@ impl MobileAuthentication {
field: u64,
value: String,
) -> Result<MobileEntryPage, MobileAuthenticationError> {
self.ensure_repository_idle()?;
let mut document = self.open_active_document(path)?;
document
.replace_field_value(EntryFieldId::from_value(field), value.into_bytes())
@@ -824,6 +855,7 @@ impl MobileAuthentication {
fields: Vec<MobileEntryEditorInput>,
) -> Result<MobileEntryPage, MobileAuthenticationError> {
self.ensure_active()?;
self.ensure_repository_idle()?;
let mut draft = self
.status()?
.editors
@@ -939,10 +971,10 @@ impl MobileAuthentication {
draft: MobileEntryDraft,
) -> Result<MobileEntryEditorSession, MobileAuthenticationError> {
let mut status = self.status()?;
if status.mutation_active {
if status.mutation_active || status.repository_operation_active {
return Err(entry_detail(
"Password Action In Progress",
"wait for the current move, copy, or delete action to finish",
"wait for the current password-store action to finish",
));
}
let id = status.next_editor_id;
@@ -960,7 +992,7 @@ impl MobileAuthentication {
fn reserve_entry_mutation(&self) -> Result<(), MobileAuthenticationError> {
let mut status = self.status()?;
// ponytail: serialize mobile mutations; use per-path reservations if concurrent UI needs it.
if status.mutation_active {
if status.mutation_active || status.repository_operation_active {
return Err(MobileAuthenticationError::new(
MobileAuthenticationErrorKind::Conflict,
"Password Action In Progress",
@@ -971,6 +1003,44 @@ impl MobileAuthentication {
Ok(())
}
pub(crate) fn reserve_repository_operation(
&self,
operation: MobileRepositoryOperation,
) -> Result<MobileRepositoryOperationGuard<'_>, MobileRepositoryOperationError> {
let mut status = self
.status
.lock()
.map_err(|_| MobileRepositoryOperationError::Unavailable)?;
let dirty_editor = status
.editors
.values()
.any(|draft| draft.document().is_modified());
if let Some(error) = repository_operation_conflict(
operation,
status.mutation_active || status.repository_operation_active,
!status.editors.is_empty(),
dirty_editor,
) {
return Err(error);
}
status.repository_operation_active = true;
drop(status);
Ok(MobileRepositoryOperationGuard {
authentication: self,
})
}
fn ensure_repository_idle(&self) -> Result<(), MobileAuthenticationError> {
if self.status()?.repository_operation_active {
Err(entry_detail(
"Repository Action In Progress",
"wait for the current commit, fetch, pull, or push action to finish",
))
} else {
Ok(())
}
}
fn release_entry_mutation(&self) {
if let Ok(mut status) = self.status.lock() {
status.mutation_active = false;
@@ -1061,6 +1131,27 @@ impl MobileAuthentication {
}
}
fn repository_operation_conflict(
operation: MobileRepositoryOperation,
busy: bool,
open_editor: bool,
dirty_editor: bool,
) -> Option<MobileRepositoryOperationError> {
if busy {
Some(MobileRepositoryOperationError::Busy)
} else if operation == MobileRepositoryOperation::Pull && open_editor {
Some(if dirty_editor {
MobileRepositoryOperationError::DirtyEditor
} else {
MobileRepositoryOperationError::OpenEditor
})
} else if operation == MobileRepositoryOperation::Push && dirty_editor {
Some(MobileRepositoryOperationError::DirtyEditor)
} else {
None
}
}
struct KeyOnlyProvider<'a> {
handle: NativeAuthenticationHandle,
fingerprint: &'a str,
@@ -1198,3 +1289,34 @@ fn entry_detail(title: &str, error: impl fmt::Display) -> MobileAuthenticationEr
error.to_string(),
)
}
#[cfg(test)]
mod tests {
use super::{
MobileRepositoryOperation, MobileRepositoryOperationError, repository_operation_conflict,
};
#[test]
fn repository_actions_protect_editors_and_serialize_mutations() {
assert_eq!(
repository_operation_conflict(MobileRepositoryOperation::Pull, false, true, false),
Some(MobileRepositoryOperationError::OpenEditor)
);
assert_eq!(
repository_operation_conflict(MobileRepositoryOperation::Pull, false, true, true),
Some(MobileRepositoryOperationError::DirtyEditor)
);
assert_eq!(
repository_operation_conflict(MobileRepositoryOperation::Push, false, true, true),
Some(MobileRepositoryOperationError::DirtyEditor)
);
assert_eq!(
repository_operation_conflict(MobileRepositoryOperation::Fetch, false, true, true),
None
);
assert_eq!(
repository_operation_conflict(MobileRepositoryOperation::Commit, true, false, false),
Some(MobileRepositoryOperationError::Busy)
);
}
}