Implement embedded Git synchronization TUI
This commit is contained in:
@@ -2,15 +2,22 @@
|
||||
|
||||
mod support;
|
||||
|
||||
use std::{error::Error, fs, io::Cursor, path::Path, sync::Mutex};
|
||||
use std::{
|
||||
error::Error,
|
||||
fs,
|
||||
io::Cursor,
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use ironstorage::{
|
||||
command::{InsertInput, InsertRequest},
|
||||
config::{Config, ConfigLoader, GitRemote},
|
||||
crypto::{DetachedSignatureBytes, KeyInfo, KeyStore, SecretProvider, SecretProviderError},
|
||||
git::{
|
||||
GitChangeKind, GitCredential, GitCredentialProvider, GitError, GitFetchTransport,
|
||||
GitIdentity, GitRepository, GitSmartHttpTransport, PullOutcome,
|
||||
GitChangeKind, GitConflictChoice, GitConflictResolution, GitCredential,
|
||||
GitCredentialProvider, GitError, GitFetchTransport, GitIdentity, GitOperationControl,
|
||||
GitProgressPhase, GitRepository, GitSmartHttpTransport, PullOutcome,
|
||||
},
|
||||
repository::{Repository, SecretBytes},
|
||||
write::{InsertContent, OverwriteDecision, VaultWriter},
|
||||
@@ -206,6 +213,19 @@ impl GitFetchTransport for NoopFetch {
|
||||
}
|
||||
}
|
||||
|
||||
struct FailedFetch(GitError);
|
||||
|
||||
impl GitFetchTransport for FailedFetch {
|
||||
fn fetch(
|
||||
&self,
|
||||
_repository: &GitRepository,
|
||||
_configured: &GitRemote,
|
||||
_credential: &GitCredential,
|
||||
) -> Result<bool, GitError> {
|
||||
Err(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationFailure;
|
||||
|
||||
impl GitSmartHttpTransport for AuthenticationFailure {
|
||||
@@ -371,13 +391,95 @@ fn fetched_branches_fast_forward_and_report_typed_conflicts() -> TestResult {
|
||||
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!(
|
||||
error,
|
||||
GitError::MergeConflicts {
|
||||
paths: vec!["secret.gpg".into()]
|
||||
}
|
||||
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);
|
||||
|
||||
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().as_str())?;
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user