#![forbid(unsafe_code)] mod support; use std::{ error::Error, fs, io::Cursor, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; use ironstorage::{ command::{InsertInput, InsertRequest}, config::{Config, ConfigLoader, GitRemote}, crypto::{DetachedSignatureBytes, KeyInfo, KeyStore, SecretProvider, SecretProviderError}, git::{ GitChangeKind, GitConflictChoice, GitConflictResolution, GitCredential, GitCredentialProvider, GitError, GitFetchTransport, GitIdentity, GitOperationControl, GitProgressPhase, GitRepository, GitSmartHttpTransport, PullOutcome, }, repository::{Repository, SecretBytes}, write::{InsertContent, OverwriteDecision, VaultWriter}, }; use pgp::{ composed::{Deserializable as _, DetachedSignature}, ser::Serialize as _, }; use sha1::Digest as _; use support::compatibility::FixtureSet; type TestResult = Result<(), Box>; fn identity() -> GitIdentity { GitIdentity::new("IronStorage Test", "test@ironstorage.invalid").expect("valid identity") } fn remote_config(temporary: &tempfile::TempDir) -> Result> { fs::create_dir_all(temporary.path().join("config/keys"))?; fs::create_dir_all(temporary.path().join("native"))?; fs::create_dir_all(temporary.path().join("vault"))?; let config_path = temporary.path().join("config/config.toml"); fs::write( &config_path, format!( "vault = {:?}\ndefault_key = \"0123456789ABCDEF0123456789ABCDEF01234567\"\nkey_material = \"keys\"\n[[git.remotes]]\nname = \"origin\"\nurl = \"https://example.test/store.git\"\nserver_id = \"server\"\napplication_id = \"application\"\n", temporary.path().join("vault") ), )?; Ok( ConfigLoader::new(temporary.path().to_owned(), temporary.path().join("native")) .load(Some(&config_path))?, ) } #[test] fn local_git_workflow_stages_commits_diffs_logs_and_deletes() -> TestResult { let temporary = tempfile::tempdir()?; let store = Repository::open(temporary.path())?; let git = GitRepository::init(&store, identity())?; fs::write(temporary.path().join(".gpg-id"), b"ALICE\n")?; git.stage(&[".gpg-id".into()])?; let first = git.commit("Set password store recipients.")?; assert_eq!(first.len(), 40); assert!(git.status()?.is_clean()); fs::write(temporary.path().join(".gpg-id"), b"BOB\n")?; let status = git.status()?; assert_eq!(status.unstaged().len(), 1); assert_eq!(status.unstaged()[0].kind(), GitChangeKind::Modified); let diff = git.diff(&[])?; assert_eq!(diff[0].old(), Some(b"ALICE\n".as_slice())); assert_eq!(diff[0].current(), Some(b"BOB\n".as_slice())); let fixture = FixtureSet::load()?; let keys = KeyStore::load(fixture.path("keys"))?; let rendered = git.render_diff(&[], &keys, &mut SigningSecret(Vec::new()))?; assert!(rendered.expose().windows(7).any(|part| part == b"-ALICE\n")); assert!(rendered.expose().windows(5).any(|part| part == b"+BOB\n")); git.stage(&[".gpg-id".into()])?; assert_eq!(git.status()?.staged()[0].kind(), GitChangeKind::Modified); git.commit("Change password store recipients.")?; assert_eq!(git.log(None)?.len(), 3); assert_eq!( git.log(Some(1))?[0].message(), "Change password store recipients." ); assert_eq!(git.stage_and_commit(&[".gpg-id".into()], "No-op")?, None); fs::remove_file(temporary.path().join(".gpg-id"))?; git.stage(&[".gpg-id".into()])?; let deletion = git.status()?; assert_eq!(deletion.staged().len(), 1, "{deletion:?}"); assert_eq!(deletion.staged()[0].kind(), GitChangeKind::Deleted); git.commit("Remove password store recipients.")?; assert!(git.status()?.is_clean()); Ok(()) } #[test] fn user_commit_stages_every_current_change_in_storage() -> TestResult { let temporary = tempfile::tempdir()?; let store = Repository::open(temporary.path())?; let git = GitRepository::init(&store, identity())?; fs::write(temporary.path().join(".gpg-id"), b"ALICE\n")?; fs::write(temporary.path().join("mail.gpg"), b"ciphertext")?; let commit = git.commit_all("Save mobile changes.")?; assert_eq!(commit.len(), 40); assert!(git.status()?.is_clean()); assert_eq!(git.log(Some(1))?[0].message(), "Save mobile changes."); assert_eq!(git.commit_all("Nothing changed"), Err(GitError::NoChanges)); Ok(()) } #[test] fn nested_repository_selection_is_innermost() -> TestResult { let temporary = tempfile::tempdir()?; let outer_store = Repository::open(temporary.path())?; GitRepository::init(&outer_store, identity())?; let root = GitRepository::open_innermost(&outer_store, Path::new(""), identity())?; assert_eq!(root.root(), fs::canonicalize(temporary.path())?); fs::create_dir(temporary.path().join("nested"))?; let inner_store = Repository::open(temporary.path().join("nested"))?; GitRepository::init(&inner_store, identity())?; fs::write(temporary.path().join("nested/secret.gpg"), b"ciphertext")?; let selected = GitRepository::open_innermost(&outer_store, Path::new("nested/secret.gpg"), identity())?; assert_eq!( selected.root(), fs::canonicalize(temporary.path().join("nested"))? ); Ok(()) } #[test] fn remotes_and_config_reject_local_helper_and_credential_urls() -> TestResult { let temporary = tempfile::tempdir()?; let store = Repository::open(temporary.path())?; let mut git = GitRepository::init(&store, identity())?; for forbidden in [ "git://example.test/store.git", "file:///tmp/store.git", "../store.git", "ext::helper command", "https://user:secret@example.test/store.git", ] { assert_eq!( git.add_remote("origin", forbidden), Err(GitError::ForbiddenRemoteUrl) ); } git.add_remote("origin", "https://example.test/store.git")?; assert_eq!(git.remotes(), ["origin"]); assert_eq!(git.remote_url("origin")?, "https://example.test/store.git"); git.set_remote_url("origin", "https://example.test/other.git")?; assert_eq!(git.remote_url("origin")?, "https://example.test/other.git"); git.config_set("user.name", "Local User")?; assert_eq!(git.config_get("user.name")?.as_deref(), Some("Local User")); assert!(git.config_set("credential.helper", "evil").is_err()); assert!( git.config_set("diff.gpg.textconv", "external-helper") .is_err() ); git.remove_remote("origin")?; assert!(git.remotes().is_empty()); Ok(()) } #[cfg(not(feature = "ssh"))] #[test] fn ssh_remotes_are_typed_but_unavailable_before_transport_or_mutation() -> TestResult { let temporary = tempfile::tempdir()?; let store = Repository::open(temporary.path())?; let mut git = GitRepository::init(&store, identity())?; let remote = GitRemote::ssh("origin", "git@example.test:team/store.git")?; let unsupported = GitError::UnsupportedRemoteTransport { transport: ironstorage::config::RemoteTransport::Ssh, }; assert_eq!( git.add_remote(remote.name().as_str(), remote.url()), Err(unsupported.clone()) ); assert!(git.remotes().is_empty()); assert_eq!( git.config_set("remote.origin.url", remote.url()), Err(unsupported.clone()) ); assert!(git.config_get("remote.origin.url")?.is_none()); assert_eq!( GitRepository::discover_remote_branches_with_transport( temporary.path(), identity(), &remote, &Credentials, &CloningFetch, &GitOperationControl::default(), ), Err(unsupported) ); Ok(()) } #[cfg(feature = "ssh")] #[test] fn ssh_feature_allows_repository_remote_configuration() -> TestResult { let temporary = tempfile::tempdir()?; let store = Repository::open(temporary.path())?; let mut git = GitRepository::init(&store, identity())?; git.add_remote("origin", "git@example.test:team/store.git")?; assert_eq!(git.remote_url("origin")?, "git@example.test:team/store.git"); Ok(()) } struct Credentials; impl GitCredentialProvider for Credentials { fn credential( &self, server: &ironstorage::config::ServerId, application: &ironstorage::config::ApplicationId, ) -> Result { assert_eq!(server.as_str(), "server"); assert_eq!(application.as_str(), "application"); GitCredential::new("alice", b"token".to_vec()) } } struct SigningSecret(Vec); impl SecretProvider for SigningSecret { fn secret_for(&mut self, _key: &KeyInfo) -> Result { Ok(SecretBytes::new(self.0.clone())) } } #[derive(Default)] struct RecordingTransport { request: Mutex>, } struct CloningFetch; impl GitFetchTransport for CloningFetch { fn fetch( &self, repository: &GitRepository, _configured: &GitRemote, credential: &GitCredential, ) -> Result { assert_eq!(credential.password(), b"token"); fs::write(repository.root().join(".gpg-id"), b"ALICE\n") .map_err(|error| GitError::InvalidRepository(error.to_string()))?; repository.stage(&[".gpg-id".into()])?; let tip = repository.commit("Fetched initial store.")?; set_remote_tracking(repository.root(), &tip) .map_err(|error| GitError::InvalidRepository(error.to_string()))?; reset_to_unborn(repository.root()) .map_err(|error| GitError::InvalidRepository(error.to_string()))?; Ok(true) } } struct OccupyingFetch(PathBuf); impl GitFetchTransport for OccupyingFetch { fn fetch( &self, repository: &GitRepository, configured: &GitRemote, credential: &GitCredential, ) -> Result { let fetched = CloningFetch.fetch(repository, configured, credential)?; fs::write(self.0.join("keep"), b"concurrent contents") .map_err(|error| GitError::InvalidRepository(error.to_string()))?; Ok(fetched) } } struct NoopFetch; impl GitFetchTransport for NoopFetch { fn fetch( &self, _repository: &GitRepository, _configured: &GitRemote, credential: &GitCredential, ) -> Result { assert_eq!(credential.username(), "alice"); Ok(false) } } struct FailedFetch(GitError); impl GitFetchTransport for FailedFetch { fn fetch( &self, _repository: &GitRepository, _configured: &GitRemote, _credential: &GitCredential, ) -> Result { Err(self.0.clone()) } } struct AuthenticationFailure; impl GitSmartHttpTransport for AuthenticationFailure { fn advertise_receive_pack( &self, _url: &url::Url, _credential: &GitCredential, ) -> Result, GitError> { Err(GitError::AuthenticationFailed) } fn receive_pack( &self, _url: &url::Url, _credential: &GitCredential, _request: Vec, ) -> Result, GitError> { unreachable!("authentication fails before receive-pack") } } struct NonFastForwardAdvertisement; impl GitSmartHttpTransport for NonFastForwardAdvertisement { fn advertise_receive_pack( &self, _url: &url::Url, _credential: &GitCredential, ) -> Result, GitError> { let mut output = packet(b"# service=git-receive-pack\n"); output.extend_from_slice(b"0000"); output.extend_from_slice(&packet( b"1111111111111111111111111111111111111111 refs/heads/main\0report-status\n", )); output.extend_from_slice(b"0000"); Ok(output) } fn receive_pack( &self, _url: &url::Url, _credential: &GitCredential, _request: Vec, ) -> Result, GitError> { unreachable!("non-fast-forward is rejected before receive-pack") } } impl GitSmartHttpTransport for RecordingTransport { fn advertise_receive_pack( &self, _url: &url::Url, credential: &GitCredential, ) -> Result, GitError> { assert_eq!(credential.username(), "alice"); let mut output = packet(b"# service=git-receive-pack\n"); output.extend_from_slice(b"0000"); output.extend_from_slice(&packet(b"0000000000000000000000000000000000000000 capabilities^{}\0report-status delete-refs\n")); output.extend_from_slice(b"0000"); Ok(output) } fn receive_pack( &self, _url: &url::Url, _credential: &GitCredential, request: Vec, ) -> Result, GitError> { *self.request.lock().expect("request lock") = request; let mut output = packet(b"unpack ok\n"); output.extend_from_slice(&packet(b"ok refs/heads/main\n")); output.extend_from_slice(b"0000"); Ok(output) } } #[test] fn injected_smart_http_push_sends_a_complete_pack_and_credentials() -> TestResult { let temporary = tempfile::tempdir()?; let config = remote_config(&temporary)?; let remote: &GitRemote = &config.git_remotes()[0]; let store = Repository::open(config.vault())?; let mut git = GitRepository::init(&store, identity())?; git.add_remote("origin", remote.url())?; fs::write(config.vault().join("secret.gpg"), b"ciphertext")?; git.stage(&["secret.gpg".into()])?; let head = git.commit("Add secret to store.")?; let transport = RecordingTransport::default(); let outcome = git.push_with_transport(remote, Some("main"), &Credentials, &transport)?; assert_eq!(outcome.new_id(), head); let request = transport.request.lock().expect("request lock"); let pack_offset = request .windows(4) .position(|window| window == b"PACK") .expect("pack payload"); assert!( request[..pack_offset] .windows(b"refs/heads/main".len()) .any(|window| window == b"refs/heads/main") ); let pack = &request[pack_offset..]; assert_eq!(&pack[..4], b"PACK"); assert_eq!(u32::from_be_bytes(pack[4..8].try_into()?), 2); assert!(u32::from_be_bytes(pack[8..12].try_into()?) >= 3); let digest = sha1::Sha1::digest(&pack[..pack.len() - 20]); assert_eq!(digest.as_slice(), &pack[pack.len() - 20..]); Ok(()) } #[test] fn controlled_sync_pulls_then_pushes_with_one_progress_and_cancellation_contract() -> TestResult { let temporary = tempfile::tempdir()?; let config = remote_config(&temporary)?; let remote = &config.git_remotes()[0]; let store = Repository::open(config.vault())?; let mut git = GitRepository::init(&store, identity())?; git.add_remote("origin", remote.url())?; let head = git.log(Some(1))?[0].id().to_owned(); set_remote_tracking(config.vault(), &head)?; let phases = Arc::new(Mutex::new(Vec::new())); let recorded = Arc::clone(&phases); let control = GitOperationControl::new(move |phase| { recorded.lock().expect("progress").push(phase); }); let push = RecordingTransport::default(); let (pull, pushed) = git.sync_with_transports_controlled(remote, &Credentials, &NoopFetch, &push, &control)?; assert_eq!(pull, PullOutcome::UpToDate); assert_eq!(pushed.new_id(), head); let phases = phases.lock().expect("progress"); assert!(phases.contains(&GitProgressPhase::Receiving)); assert!(phases.contains(&GitProgressPhase::Integrating)); assert!(phases.contains(&GitProgressPhase::Sending)); assert!(!push.request.lock().expect("push request").is_empty()); Ok(()) } #[test] fn push_propagates_authentication_and_rejects_non_fast_forward_before_upload() -> TestResult { let temporary = tempfile::tempdir()?; let config = remote_config(&temporary)?; let remote = &config.git_remotes()[0]; let store = Repository::open(config.vault())?; let mut git = GitRepository::init(&store, identity())?; git.add_remote("origin", remote.url())?; assert_eq!( git.push_with_transport(remote, Some("main"), &Credentials, &AuthenticationFailure), Err(GitError::AuthenticationFailed) ); assert_eq!( git.push_with_transport( remote, Some("main"), &Credentials, &NonFastForwardAdvertisement, ), Err(GitError::NonFastForward) ); Ok(()) } #[test] fn fetched_branches_fast_forward_and_report_typed_conflicts() -> TestResult { let temporary = tempfile::tempdir()?; let config = remote_config(&temporary)?; let remote = &config.git_remotes()[0]; let store = Repository::open(config.vault())?; let mut git = GitRepository::init(&store, identity())?; git.add_remote("origin", remote.url())?; fs::write(config.vault().join("secret.gpg"), b"base")?; git.stage(&["secret.gpg".into()])?; let base = git.commit("Base")?; fs::write(config.vault().join("secret.gpg"), b"remote")?; git.stage(&["secret.gpg".into()])?; let remote_tip = git.commit("Remote change")?; reset_head_and_index(&git, config.vault(), &base, b"base")?; set_remote_tracking(config.vault(), &remote_tip)?; assert_eq!( git.pull_with_transport(remote, Some("main"), &Credentials, &NoopFetch)?, PullOutcome::FastForward ); assert_eq!(fs::read(config.vault().join("secret.gpg"))?, b"remote"); reset_head_and_index(&git, config.vault(), &base, b"base")?; fs::write(config.vault().join("secret.gpg"), b"local")?; git.stage(&["secret.gpg".into()])?; git.commit("Local change")?; let error = git .pull_with_transport(remote, Some("main"), &Credentials, &NoopFetch) .expect_err("conflicting histories"); let GitError::MergeConflicts { conflicts } = error else { panic!("expected typed merge conflicts"); }; assert_eq!(conflicts.len(), 1); assert_eq!(conflicts[0].path(), Path::new("secret.gpg")); assert_eq!( conflicts[0].kind(), ironstorage::git::GitConflictKind::Content ); assert_eq!(fs::read(config.vault().join("secret.gpg"))?, b"local"); let snapshot = git.snapshot(Some(remote), 5)?; assert_eq!(snapshot.remote().expect("remote status").ahead(), 1); assert_eq!(snapshot.remote().expect("remote status").behind(), 1); let divergence = git.divergence(remote, 10)?; assert_eq!(divergence.remote().ahead(), 1); assert_eq!(divergence.remote().behind(), 1); assert_eq!(divergence.incoming().len(), 1); assert_eq!(divergence.incoming()[0].commit().message(), "Remote change"); assert_eq!(divergence.incoming()[0].changes().len(), 1); assert_eq!( divergence.incoming()[0].changes()[0].path(), Path::new("secret.gpg") ); assert_eq!(divergence.outgoing().len(), 1); assert_eq!(divergence.outgoing()[0].commit().message(), "Local change"); let totals_only = git.divergence(remote, 0)?; assert_eq!(totals_only.remote().ahead(), 1); assert_eq!(totals_only.remote().behind(), 1); assert!(totals_only.incoming().is_empty()); assert!(totals_only.outgoing().is_empty()); assert_eq!( git.resolve_fetched( remote, Some("main"), &[GitConflictResolution::new( "secret.gpg".into(), GitConflictChoice::Remote, )], )?, PullOutcome::Merged ); assert_eq!(fs::read(config.vault().join("secret.gpg"))?, b"remote"); assert_eq!(git.log(Some(1))?[0].parents().len(), 2); Ok(()) } #[test] fn cancelled_operations_stop_before_credentials_or_transport() -> TestResult { let temporary = tempfile::tempdir()?; let config = remote_config(&temporary)?; let remote = &config.git_remotes()[0]; let store = Repository::open(config.vault())?; let mut git = GitRepository::init(&store, identity())?; git.add_remote("origin", remote.url())?; let control = GitOperationControl::default(); control.cancel(); assert_eq!( git.pull_with_transport_controlled( remote, Some("main"), &Credentials, &NoopFetch, &control, ), Err(GitError::Cancelled) ); assert!(git.status()?.is_clean()); let phases = Arc::new(Mutex::new(Vec::new())); let recorded = Arc::clone(&phases); let progress = GitOperationControl::new(move |phase| { recorded.lock().expect("progress lock").push(phase); }); assert!(matches!( git.pull_with_transport_controlled( remote, Some("main"), &Credentials, &NoopFetch, &progress, ), Err(GitError::RemoteNotFound { .. }) )); assert_eq!( *phases.lock().expect("progress lock"), [ GitProgressPhase::Validating, GitProgressPhase::Authenticating, GitProgressPhase::Receiving, GitProgressPhase::Receiving, GitProgressPhase::Integrating, ] ); for expected in [GitError::NetworkUnavailable, GitError::TlsFailed] { assert_eq!( git.pull_with_transport( remote, Some("main"), &Credentials, &FailedFetch(expected.clone()), ), Err(expected) ); } let credential = GitCredential::new("alice", b"DO-NOT-RENDER".to_vec())?; assert_eq!(format!("{credential:?}"), "GitCredential([REDACTED])"); Ok(()) } #[test] fn clone_uses_a_private_directory_and_injected_fetch_transport() -> TestResult { let temporary = tempfile::tempdir()?; let config = remote_config(&temporary)?; let destination = temporary.path().join("cloned-vault"); let cloned = GitRepository::clone_into_with_transport( &destination, identity(), &config.git_remotes()[0], &Credentials, &CloningFetch, )?; assert_eq!(cloned.root(), destination); assert_eq!(fs::read(destination.join(".gpg-id"))?, b"ALICE\n"); assert_eq!(cloned.remotes(), ["origin"]); assert_eq!(cloned.log(None)?.len(), 1); assert_eq!(cloned.log(None)?[0].message(), "Fetched initial store."); assert!(!destination.join(".gitattributes").exists()); assert!( !temporary .path() .read_dir()? .filter_map(Result::ok) .any(|entry| entry .file_name() .to_string_lossy() .starts_with(".ironstorage-clone-")) ); Ok(()) } #[test] fn branch_discovery_and_controlled_clone_stay_inside_rust() -> TestResult { let temporary = tempfile::tempdir()?; let config = remote_config(&temporary)?; let remote = &config.git_remotes()[0]; let control = GitOperationControl::default(); let branches = GitRepository::discover_remote_branches_with_transport( temporary.path(), identity(), remote, &Credentials, &CloningFetch, &control, )?; assert_eq!(branches, ["main"]); assert!( !temporary .path() .read_dir()? .filter_map(Result::ok) .any(|entry| { entry .file_name() .to_string_lossy() .starts_with(".ironstorage-probe-") }) ); let destination = temporary.path().join("selected-branch"); let cloned = GitRepository::clone_into_with_transport_controlled( &destination, identity(), remote, Some(&branches[0]), &Credentials, &CloningFetch, &control, )?; assert_eq!(cloned.current_branch()?, "main"); assert_eq!(fs::read(destination.join(".gpg-id"))?, b"ALICE\n"); Ok(()) } #[test] fn cancelled_clone_never_touches_an_existing_destination() -> TestResult { let temporary = tempfile::tempdir()?; let config = remote_config(&temporary)?; let destination = temporary.path().join("existing"); fs::create_dir(&destination)?; fs::write(destination.join("keep"), b"unchanged")?; let control = GitOperationControl::default(); control.cancel(); let result = GitRepository::clone_into_with_transport_controlled( &destination, identity(), &config.git_remotes()[0], Some("main"), &Credentials, &CloningFetch, &control, ); assert!(matches!(result, Err(GitError::Cancelled))); assert_eq!(fs::read(destination.join("keep"))?, b"unchanged"); Ok(()) } #[test] fn clone_race_preserves_the_destination_and_removes_private_work() -> TestResult { let temporary = tempfile::tempdir()?; let config = remote_config(&temporary)?; let destination = temporary.path().join("concurrent"); fs::create_dir(&destination)?; let result = GitRepository::clone_into_with_transport( &destination, identity(), &config.git_remotes()[0], &Credentials, &OccupyingFetch(destination.clone()), ); assert!(matches!(result, Err(GitError::Io { .. }))); assert_eq!(fs::read(destination.join("keep"))?, b"concurrent contents"); assert!( !temporary .path() .read_dir()? .filter_map(Result::ok) .any(|entry| { entry .file_name() .to_string_lossy() .starts_with(".ironstorage-clone-") }) ); Ok(()) } #[test] fn signed_commits_have_a_verifiable_ascii_armored_gpgsig() -> TestResult { let fixture = FixtureSet::load()?; let alice = fixture.key("alice")?; let keys = KeyStore::load(fixture.path("keys"))?; let signer = keys.resolve(&alice.primary_fingerprint)?; let temporary = tempfile::tempdir()?; let store = Repository::open(temporary.path())?; let git = GitRepository::init(&store, identity())?; fs::write(temporary.path().join("secret.gpg"), b"ciphertext")?; git.stage(&["secret.gpg".into()])?; git.commit_signed( "Add signed password.", &keys, &signer, &mut SigningSecret(alice.passphrase.as_bytes().to_vec()), )?; let repository = open_test_git(temporary.path())?; let head = repository.head_commit()?; let decoded = head.decode()?; let mut commit = decoded.into_owned()?; let signature_index = commit .extra_headers .iter() .position(|(name, _)| name.as_slice() == b"gpgsig") .expect("gpgsig header"); let armor = commit.extra_headers.remove(signature_index).1; assert!(armor.starts_with(b"-----BEGIN PGP SIGNATURE-----")); let (signature, _) = DetachedSignature::from_armor_single(Cursor::new(armor))?; let mut signature_bytes = Vec::new(); signature.to_writer(&mut signature_bytes)?; let mut unsigned = Vec::new(); gix::objs::WriteTo::write_to(&commit, &mut unsigned)?; assert_eq!( keys.verify( &unsigned, &DetachedSignatureBytes::new(signature_bytes), std::slice::from_ref(&signer), )?, signer ); Ok(()) } #[test] fn successful_storage_mutations_use_the_embedded_committer() -> TestResult { let fixture = FixtureSet::load()?; let store = fixture.materialize_store("basic")?; let repository = Repository::open(store.path())?; let keys = KeyStore::load(fixture.path("keys"))?; let mut git = GitRepository::init(&repository, identity())?; assert_eq!( fs::read(store.path().join(".gitattributes"))?, b"*.gpg diff=gpg\n" ); let initialized = git.log(None)?; assert_eq!(initialized.len(), 2); assert_eq!( initialized[0].message(), "Configure git repository for gpg file diff." ); assert_eq!( initialized[1].message(), "Add current contents of password store." ); fs::write(store.path().join("unrelated.gpg"), b"separately staged")?; git.stage(&["unrelated.gpg".into()])?; let writer = VaultWriter::new(&repository, &keys); writer.insert( &InsertRequest { entry: "automatic/entry".to_owned(), input: InsertInput::EchoedLine, force: false, }, InsertContent::echoed(b"generated secret".to_vec())?, OverwriteDecision::Decline, None, &mut git, )?; let log = git.log(None)?; assert_eq!(log.len(), 3); assert_eq!( log[0].message(), "Add given password for automatic/entry to store." ); let embedded = open_test_git(store.path())?; let head = embedded.head_commit()?; let committed = head.tree()?; assert!( committed .lookup_entry_by_path("automatic/entry.gpg")? .is_some() ); assert!(committed.lookup_entry_by_path("unrelated.gpg")?.is_none()); let status = git.status()?; assert_eq!(status.staged().len(), 1, "{status:?}"); assert_eq!(status.staged()[0].path(), Path::new("unrelated.gpg")); assert_eq!(status.staged()[0].kind(), GitChangeKind::Added); Ok(()) } fn reset_head_and_index(git: &GitRepository, root: &Path, id: &str, contents: &[u8]) -> TestResult { let repository = open_test_git(root)?; let id = gix::hash::ObjectId::from_hex(id.as_bytes())?; repository .head_ref()? .expect("born HEAD") .set_target_id(id, "test reset")?; fs::write(root.join("secret.gpg"), contents)?; git.stage(&["secret.gpg".into()])?; Ok(()) } fn set_remote_tracking(root: &Path, id: &str) -> TestResult { let repository = open_test_git(root)?; let id = gix::hash::ObjectId::from_hex(id.as_bytes())?; if let Some(existing) = repository.try_find_reference("refs/remotes/origin/main")? { existing.delete()?; } repository.reference( "refs/remotes/origin/main", id, gix::refs::transaction::PreviousValue::MustNotExist, "test remote update", )?; Ok(()) } fn reset_to_unborn(root: &Path) -> TestResult { let repository = open_test_git(root)?; repository.find_reference("refs/heads/main")?.delete()?; let mut index = repository.index_from_tree(&repository.empty_tree().id)?; index.write(Default::default())?; fs::remove_file(root.join(".gpg-id"))?; Ok(()) } fn open_test_git(root: &Path) -> Result> { Ok(gix::open_opts( root, gix::open::Options::isolated().config_overrides([ "user.name=Test".to_owned(), "user.email=test@ironstorage.invalid".to_owned(), ]), )?) } fn packet(data: &[u8]) -> Vec { let mut output = format!("{:04x}", data.len() + 4).into_bytes(); output.extend_from_slice(data); output }