Implement embedded Git synchronization TUI

This commit is contained in:
Hermes Agent
2026-08-10 10:39:31 +00:00
parent ce3e4a9f79
commit 1850846696
8 changed files with 1402 additions and 36 deletions

View File

@@ -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(())
}