Sync Home with pull then push and prepare 1.0.1 build 8 (#124)
Dependency security audit / rustsec (push) Successful in 20s
Tagged release / prepare-release (push) Successful in 4s
Tagged release / build-unix (push) Successful in 1h15m17s
Tagged release / build-windows (push) Successful in 19m26s
Tagged release / publish-release (push) Successful in 3s
Dependency security audit / rustsec (push) Successful in 20s
Tagged release / prepare-release (push) Successful in 4s
Tagged release / build-unix (push) Successful in 1h15m17s
Tagged release / build-windows (push) Successful in 19m26s
Tagged release / publish-release (push) Successful in 3s
This commit is contained in:
1 parent
3c3574b449
commit
aec3f237a9
12 files changed
+1174
-50
No files matched your search
@@ -482,6 +482,10 @@ impl MobileHomeOperation {
|
||||
self.operation.push().map(Into::into).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn sync(&self) -> Result<MobileHomePage, MobileHomeFfiError> {
|
||||
self.operation.sync().map(Into::into).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.operation.cancel();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Storage-owned remote activity and pull-to-refresh models for iPhone Home.
|
||||
//! Storage-owned remote activity and pull-then-push refresh for iPhone Home.
|
||||
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
@@ -412,6 +412,13 @@ impl MobileHomeOperation {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sync(&self) -> Result<MobileHomePage, MobileHomeError> {
|
||||
// Hold the pull reservation across both steps: sync can replace local entries.
|
||||
self.run(MobileHomeActionKind::Pull, || {
|
||||
MobileHomeStorage::load()?.sync(unix_seconds()?, &self.control)
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
action: MobileHomeActionKind,
|
||||
@@ -555,6 +562,27 @@ impl MobileHomeStorage {
|
||||
.map_err(MobileHomeError::after_push)
|
||||
}
|
||||
|
||||
fn sync(
|
||||
&self,
|
||||
now: i64,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<MobileHomePage, MobileHomeError> {
|
||||
let remote = self.remote()?;
|
||||
let store = self.credentials()?;
|
||||
let synced = self.git.sync_controlled(remote, &store, control);
|
||||
let locked = store.lock();
|
||||
let (_, pushed) = synced.map_err(MobileHomeError::from_git)?;
|
||||
locked.map_err(MobileHomeError::partial_secret)?;
|
||||
control
|
||||
.report(GitProgressPhase::Refreshing)
|
||||
.map_err(MobileHomeError::from_git)?;
|
||||
let mut notice = push_notice(&pushed);
|
||||
notice.title = "Sync Completed".to_owned();
|
||||
notice.detail = format!("Pull completed successfully. {}", notice.detail);
|
||||
self.current_page(now, Some(notice))
|
||||
.map_err(MobileHomeError::after_push)
|
||||
}
|
||||
|
||||
fn credentials(&self) -> Result<NativeSecretStore, MobileHomeError> {
|
||||
let store = NativeSecretStore::system(
|
||||
SecretCachePolicy::Disabled,
|
||||
|
||||
@@ -265,6 +265,7 @@ impl SecretProvider for SigningSecret {
|
||||
#[derive(Default)]
|
||||
struct RecordingTransport {
|
||||
request: Mutex<Vec<u8>>,
|
||||
remote_tip: Option<String>,
|
||||
}
|
||||
|
||||
struct CloningFetch;
|
||||
@@ -391,7 +392,13 @@ impl GitSmartHttpTransport for RecordingTransport {
|
||||
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"));
|
||||
let reference = self.remote_tip.as_ref().map_or_else(
|
||||
|| "0000000000000000000000000000000000000000 capabilities^{}".to_owned(),
|
||||
|tip| format!("{tip} refs/heads/main"),
|
||||
);
|
||||
output.extend_from_slice(&packet(
|
||||
format!("{reference}\0report-status delete-refs\n").as_bytes(),
|
||||
));
|
||||
output.extend_from_slice(b"0000");
|
||||
Ok(output)
|
||||
}
|
||||
@@ -444,6 +451,61 @@ fn injected_smart_http_push_sends_a_complete_pack_and_credentials() -> TestResul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_pushes_move_and_move_back_even_when_the_final_tree_is_unchanged() -> 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"ciphertext")?;
|
||||
let base = git.commit_all("Add secret")?;
|
||||
set_remote_tracking(config.vault(), &base)?;
|
||||
fs::rename(
|
||||
config.vault().join("secret.gpg"),
|
||||
config.vault().join("moved.gpg"),
|
||||
)?;
|
||||
git.commit_all("Move secret")?;
|
||||
fs::rename(
|
||||
config.vault().join("moved.gpg"),
|
||||
config.vault().join("secret.gpg"),
|
||||
)?;
|
||||
let head = git.commit_all("Move secret back")?;
|
||||
assert!(git.status()?.is_clean());
|
||||
assert_eq!(git.divergence(remote, 10)?.remote().ahead(), 2);
|
||||
let transport = RecordingTransport {
|
||||
remote_tip: Some(base.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
let (pulled, pushed) = git.sync_with_transports_controlled(
|
||||
remote,
|
||||
&Credentials,
|
||||
&NoopFetch,
|
||||
&transport,
|
||||
&GitOperationControl::default(),
|
||||
)?;
|
||||
assert_eq!(pulled, PullOutcome::UpToDate);
|
||||
assert_eq!(pushed.new_id(), head);
|
||||
let request = transport.request.lock().expect("request lock");
|
||||
assert!(
|
||||
request
|
||||
.windows(base.len())
|
||||
.any(|bytes| bytes == base.as_bytes())
|
||||
);
|
||||
let offset = request
|
||||
.windows(4)
|
||||
.position(|bytes| bytes == b"PACK")
|
||||
.expect("pack");
|
||||
// Two commits and the intermediate tree; the remote already has the final tree and blob.
|
||||
assert_eq!(
|
||||
u32::from_be_bytes(request[offset + 8..offset + 12].try_into()?),
|
||||
3
|
||||
);
|
||||
assert_eq!(git.divergence(remote, 10)?.remote().ahead(), 0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controlled_sync_pulls_then_pushes_with_one_progress_and_cancellation_contract() -> TestResult {
|
||||
let temporary = tempfile::tempdir()?;
|
||||
@@ -469,6 +531,20 @@ fn controlled_sync_pulls_then_pushes_with_one_progress_and_cancellation_contract
|
||||
assert!(phases.contains(&GitProgressPhase::Integrating));
|
||||
assert!(phases.contains(&GitProgressPhase::Sending));
|
||||
assert!(!push.request.lock().expect("push request").is_empty());
|
||||
for error in [GitError::NetworkUnavailable, GitError::AuthenticationFailed] {
|
||||
let push = RecordingTransport::default();
|
||||
assert_eq!(
|
||||
git.sync_with_transports_controlled(
|
||||
remote,
|
||||
&Credentials,
|
||||
&FailedFetch(error.clone()),
|
||||
&push,
|
||||
&GitOperationControl::default(),
|
||||
),
|
||||
Err(error),
|
||||
);
|
||||
assert!(push.request.lock().expect("push request").is_empty());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -524,9 +600,17 @@ fn fetched_branches_fast_forward_and_report_typed_conflicts() -> TestResult {
|
||||
fs::write(config.vault().join("secret.gpg"), b"local")?;
|
||||
git.stage(&["secret.gpg".into()])?;
|
||||
git.commit("Local change")?;
|
||||
let push = RecordingTransport::default();
|
||||
let error = git
|
||||
.pull_with_transport(remote, Some("main"), &Credentials, &NoopFetch)
|
||||
.sync_with_transports_controlled(
|
||||
remote,
|
||||
&Credentials,
|
||||
&NoopFetch,
|
||||
&push,
|
||||
&GitOperationControl::default(),
|
||||
)
|
||||
.expect_err("conflicting histories");
|
||||
assert!(push.request.lock().expect("push request").is_empty());
|
||||
let GitError::MergeConflicts { conflicts } = error else {
|
||||
panic!("expected typed merge conflicts");
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user