Configure iPhone Git commit identity (#76)
This commit is contained in:
@@ -16,10 +16,13 @@ use cap_tempfile::TempFile;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::authentication::{AuthenticationTimeout, DEFAULT_AUTHENTICATION_TIMEOUT};
|
||||
use crate::mobile::MobileTab;
|
||||
use crate::presentation::{ClipboardTimeout, DEFAULT_CLIPBOARD_TIMEOUT};
|
||||
use crate::repository::EntryPath;
|
||||
use crate::{
|
||||
authentication::{AuthenticationTimeout, DEFAULT_AUTHENTICATION_TIMEOUT},
|
||||
git::GitIdentity,
|
||||
mobile::MobileTab,
|
||||
presentation::{ClipboardTimeout, DEFAULT_CLIPBOARD_TIMEOUT},
|
||||
repository::EntryPath,
|
||||
};
|
||||
|
||||
const APPLICATION_DIRECTORY: &str = "ironstorage";
|
||||
const CONFIG_FILE: &str = "config.toml";
|
||||
@@ -40,6 +43,7 @@ pub struct Config {
|
||||
mobile_tab: MobileTab,
|
||||
mobile_home_refreshed_at: Option<i64>,
|
||||
watch_shared_totp_entries: BTreeSet<EntryPath>,
|
||||
git_identity: GitIdentity,
|
||||
git_remotes: Vec<GitRemote>,
|
||||
}
|
||||
|
||||
@@ -145,6 +149,10 @@ impl Config {
|
||||
&self.git_remotes
|
||||
}
|
||||
|
||||
pub fn git_identity(&self) -> &GitIdentity {
|
||||
&self.git_identity
|
||||
}
|
||||
|
||||
pub fn settings(&self) -> ConfigSettings {
|
||||
let editor = self.editor.as_ref().map(|editor| {
|
||||
std::iter::once(editor.program.clone())
|
||||
@@ -272,6 +280,35 @@ impl Config {
|
||||
validate_config(self.source.clone(), document, raw)?.persist()
|
||||
}
|
||||
|
||||
pub fn update_git_identity(&self, identity: &GitIdentity) -> Result<(), ConfigError> {
|
||||
let mut document = self.current_document()?;
|
||||
let root = document
|
||||
.as_table_mut()
|
||||
.ok_or_else(|| ConfigError::Malformed {
|
||||
path: self.source.clone(),
|
||||
})?;
|
||||
let git = root
|
||||
.entry("git")
|
||||
.or_insert_with(|| toml::Value::Table(toml::Table::new()))
|
||||
.as_table_mut()
|
||||
.ok_or(ConfigError::InvalidField { field: "git" })?;
|
||||
git.insert(
|
||||
"user_name".to_owned(),
|
||||
toml::Value::String(identity.name().to_owned()),
|
||||
);
|
||||
git.insert(
|
||||
"user_email".to_owned(),
|
||||
toml::Value::String(identity.email().to_owned()),
|
||||
);
|
||||
let raw = document
|
||||
.clone()
|
||||
.try_into::<RawConfig>()
|
||||
.map_err(|_| ConfigError::Malformed {
|
||||
path: self.source.clone(),
|
||||
})?;
|
||||
validate_config(self.source.clone(), document, raw)?.persist()
|
||||
}
|
||||
|
||||
fn current_document(&self) -> Result<toml::Value, ConfigError> {
|
||||
Self::load(Some(&self.source)).map(|config| config.document)
|
||||
}
|
||||
@@ -930,6 +967,8 @@ enum RawEditor {
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawGit {
|
||||
user_name: Option<String>,
|
||||
user_email: Option<String>,
|
||||
#[serde(default)]
|
||||
remotes: Vec<RawGitRemote>,
|
||||
}
|
||||
@@ -1019,6 +1058,19 @@ fn validate_config(
|
||||
})
|
||||
})
|
||||
.collect::<Result<BTreeSet<_>, _>>()?;
|
||||
let git_identity = match (raw.git.user_name, raw.git.user_email) {
|
||||
(None, None) => GitIdentity::ironstorage(),
|
||||
(Some(name), Some(email)) => {
|
||||
GitIdentity::new(name, email).map_err(|_| ConfigError::InvalidField {
|
||||
field: "git.user_identity",
|
||||
})?
|
||||
}
|
||||
_ => {
|
||||
return Err(ConfigError::InvalidField {
|
||||
field: "git.user_identity",
|
||||
});
|
||||
}
|
||||
};
|
||||
let git_remotes = validate_remotes(raw.git.remotes)?;
|
||||
|
||||
Ok(Config {
|
||||
@@ -1034,6 +1086,7 @@ fn validate_config(
|
||||
mobile_tab,
|
||||
mobile_home_refreshed_at,
|
||||
watch_shared_totp_entries,
|
||||
git_identity,
|
||||
git_remotes,
|
||||
})
|
||||
}
|
||||
@@ -1228,7 +1281,7 @@ fn validate_known_fields(value: &toml::Value, source: &Path) -> Result<(), Confi
|
||||
let git = git.as_table().ok_or_else(|| ConfigError::Malformed {
|
||||
path: source.to_owned(),
|
||||
})?;
|
||||
validate_table(git, "git", &["remotes"])?;
|
||||
validate_table(git, "git", &["user_name", "user_email", "remotes"])?;
|
||||
if let Some(remotes) = git.get("remotes") {
|
||||
let remotes = remotes.as_array().ok_or_else(|| ConfigError::Malformed {
|
||||
path: source.to_owned(),
|
||||
|
||||
@@ -190,6 +190,7 @@ struct ActiveMobileLease {
|
||||
|
||||
struct MobileAuthenticationStatus {
|
||||
biometric_unlock_enabled: bool,
|
||||
git_identity: GitIdentity,
|
||||
active: Option<ActiveMobileLease>,
|
||||
next_editor_id: u64,
|
||||
editors: BTreeMap<u64, MobileEntryDraft>,
|
||||
@@ -226,6 +227,7 @@ impl MobileAuthentication {
|
||||
Ok(Self {
|
||||
status: Mutex::new(MobileAuthenticationStatus {
|
||||
biometric_unlock_enabled: config.biometric_unlock_enabled(),
|
||||
git_identity: config.git_identity().clone(),
|
||||
active: None,
|
||||
next_editor_id: 0,
|
||||
editors: BTreeMap::new(),
|
||||
@@ -394,6 +396,29 @@ impl MobileAuthentication {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn git_identity(&self) -> Result<GitIdentity, MobileAuthenticationError> {
|
||||
Ok(self.status()?.git_identity.clone())
|
||||
}
|
||||
|
||||
pub fn set_git_identity(
|
||||
&self,
|
||||
name: String,
|
||||
email: String,
|
||||
) -> Result<GitIdentity, MobileAuthenticationError> {
|
||||
let identity = GitIdentity::new(name, email).map_err(|_| {
|
||||
MobileAuthenticationError::new(
|
||||
MobileAuthenticationErrorKind::Configuration,
|
||||
"Git Identity Is Invalid",
|
||||
"Enter a non-empty name and email without line breaks or angle brackets.",
|
||||
)
|
||||
})?;
|
||||
self.config
|
||||
.update_git_identity(&identity)
|
||||
.map_err(config_error)?;
|
||||
self.status()?.git_identity = identity.clone();
|
||||
Ok(identity)
|
||||
}
|
||||
|
||||
pub fn touch_user_activity(&self) -> Result<(), MobileAuthenticationError> {
|
||||
let status = self.status()?;
|
||||
let active = status.active.as_ref().ok_or_else(|| {
|
||||
@@ -483,17 +508,18 @@ impl MobileAuthentication {
|
||||
&self,
|
||||
request: MobileMutationRequest,
|
||||
) -> Result<MobileMutationOutcome, MobileAuthenticationError> {
|
||||
let (handle, key) = {
|
||||
let (handle, key, identity) = {
|
||||
let status = self.status()?;
|
||||
let active = status.active.as_ref().ok_or_else(locked_error)?;
|
||||
(active.handle.clone(), active.key.clone())
|
||||
(
|
||||
active.handle.clone(),
|
||||
active.key.clone(),
|
||||
status.git_identity.clone(),
|
||||
)
|
||||
};
|
||||
let mut committer = AutomaticTreeCommitter::for_source(
|
||||
&self.repository,
|
||||
&request.source,
|
||||
GitIdentity::ironstorage(),
|
||||
)
|
||||
.map_err(git_mutation_error)?;
|
||||
let mut committer =
|
||||
AutomaticTreeCommitter::for_source(&self.repository, &request.source, identity)
|
||||
.map_err(git_mutation_error)?;
|
||||
let has_open_editor = self.editor_state(&request.source)?.0;
|
||||
let editors = if has_open_editor && request.discard_editor {
|
||||
self.take_entry_editors(&request.source)?
|
||||
@@ -632,9 +658,9 @@ impl MobileAuthentication {
|
||||
document
|
||||
.replace_field_value(EntryFieldId::from_value(field), value.into_bytes())
|
||||
.map_err(document_error)?;
|
||||
let mut committer =
|
||||
AutomaticEntryCommitter::for_entry(&self.repository, path, GitIdentity::ironstorage())
|
||||
.map_err(|error| entry_detail("Password Entry Could Not Be Saved", error))?;
|
||||
let identity = self.git_identity()?;
|
||||
let mut committer = AutomaticEntryCommitter::for_entry(&self.repository, path, identity)
|
||||
.map_err(|error| entry_detail("Password Entry Could Not Be Saved", error))?;
|
||||
EntryDocumentService::new(&self.repository, &self.keys)
|
||||
.save_recoverable(&document, None, &mut committer)
|
||||
.map_err(document_error)?;
|
||||
@@ -795,17 +821,15 @@ impl MobileAuthentication {
|
||||
return Err(editor_error(error));
|
||||
}
|
||||
let path = draft.document().path().to_string();
|
||||
let mut committer = match AutomaticEntryCommitter::for_entry(
|
||||
&self.repository,
|
||||
&path,
|
||||
GitIdentity::ironstorage(),
|
||||
) {
|
||||
Ok(committer) => committer,
|
||||
Err(error) => {
|
||||
self.restore_editor(editor, draft)?;
|
||||
return Err(entry_detail("Password Entry Could Not Be Saved", error));
|
||||
}
|
||||
};
|
||||
let identity = self.git_identity()?;
|
||||
let mut committer =
|
||||
match AutomaticEntryCommitter::for_entry(&self.repository, &path, identity) {
|
||||
Ok(committer) => committer,
|
||||
Err(error) => {
|
||||
self.restore_editor(editor, draft)?;
|
||||
return Err(entry_detail("Password Entry Could Not Be Saved", error));
|
||||
}
|
||||
};
|
||||
if let Err(error) = EntryDocumentService::new(&self.repository, &self.keys)
|
||||
.save_recoverable(draft.document(), None, &mut committer)
|
||||
{
|
||||
|
||||
@@ -12,8 +12,8 @@ use std::{
|
||||
use crate::{
|
||||
config::{Config, ConfigError, GitRemote},
|
||||
git::{
|
||||
GitChangeKind, GitCommitActivity, GitDivergence, GitError, GitIdentity,
|
||||
GitOperationControl, GitProgressPhase, GitRepository, PullOutcome,
|
||||
GitChangeKind, GitCommitActivity, GitDivergence, GitError, GitOperationControl,
|
||||
GitProgressPhase, GitRepository, PullOutcome,
|
||||
},
|
||||
repository::{Repository, RepositoryError},
|
||||
secret_store::{
|
||||
@@ -343,7 +343,7 @@ impl MobileHomeStorage {
|
||||
let config = Config::load(None).map_err(MobileHomeError::from_config)?;
|
||||
let repository =
|
||||
Repository::open(config.vault()).map_err(MobileHomeError::from_repository)?;
|
||||
let git = GitRepository::open(&repository, GitIdentity::ironstorage())
|
||||
let git = GitRepository::open(&repository, config.git_identity().clone())
|
||||
.map_err(MobileHomeError::from_git)?;
|
||||
let remote = config
|
||||
.git_remote(None)
|
||||
|
||||
@@ -7,6 +7,7 @@ use ironstorage::{
|
||||
authentication::{DEFAULT_AUTHENTICATION_TIMEOUT, MAX_AUTHENTICATION_TIMEOUT},
|
||||
config::{ConfigError, ConfigLoader, EditorSource},
|
||||
desktop::DesktopStorage,
|
||||
git::GitIdentity,
|
||||
mobile::MobileTab,
|
||||
repository::EntryPath,
|
||||
};
|
||||
@@ -98,6 +99,38 @@ fn explicit_relative_configuration_resolves_deterministically() -> TestResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_commit_identity_defaults_validates_and_persists() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
fixture.write_explicit(fixture.valid_contents())?;
|
||||
let config = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||
assert_eq!(config.git_identity(), &GitIdentity::ironstorage());
|
||||
|
||||
let identity = GitIdentity::new("Alice Example", "alice@example.test")?;
|
||||
config.update_git_identity(&identity)?;
|
||||
let reloaded = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||
assert_eq!(reloaded.git_identity(), &identity);
|
||||
assert_eq!(reloaded.git_remotes(), config.git_remotes());
|
||||
let contents = fs::read_to_string(fixture.explicit_path())?;
|
||||
assert!(contents.contains("user_name = \"Alice Example\""));
|
||||
assert!(contents.contains("user_email = \"alice@example.test\""));
|
||||
|
||||
fixture.write_explicit(&fixture.valid_contents().replace(
|
||||
"[[git.remotes]]",
|
||||
"[git]\nuser_name = \"Alice Example\"\n\n[[git.remotes]]",
|
||||
))?;
|
||||
assert_eq!(
|
||||
fixture
|
||||
.loader()
|
||||
.load(Some(&fixture.explicit_path()))
|
||||
.expect_err("partial Git identity must be rejected"),
|
||||
ConfigError::InvalidField {
|
||||
field: "git.user_identity"
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn watch_totp_selection_persists_only_in_application_configuration() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
|
||||
Reference in New Issue
Block a user