agent: Support remote archive thread git operations (#54001)

Anthony Eid created

### Summary

This PR implements remote support for git store operations that the
sidebar's archive thread checkpoint/restore featured relied on. This was
the second to last blocker for remote usage of this feature.

I also made a shared backend between `update_ref` and `delete_ref`
called `edit_ref` because they run the same git command and this allowed
for some code unification.

#### Remote Git Operations 
- `Repository::update_ref`
- `Repository::delete_ref`
- `Repository::repair_worktrees`
- `Repository::create_archive_checkpoint`
- `Repository::restore_archive_checkpoint`


#### Follow up

`agent_ui::thread_worktree_archive::find_or_create_repository` needs to
be made aware of the remote machine that the repository it's searching
for is on. Once that is completed, we can get the correct repo when
archiving a remote thread and the flow should work without any problems.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

Change summary

crates/collab/src/rpc.rs                         |   4 
crates/fs/src/fake_git_repo.rs                   |  28 +-
crates/git/src/repository.rs                     |  48 +++-
crates/project/src/git_store.rs                  | 171 +++++++++++++++--
crates/proto/proto/git.proto                     |  33 +++
crates/proto/proto/zed.proto                     |   7 
crates/proto/src/proto.rs                        |  16 +
crates/remote_server/src/remote_editing_tests.rs |  92 +++++++++
8 files changed, 345 insertions(+), 54 deletions(-)

Detailed changes

crates/collab/src/rpc.rs 🔗

@@ -439,6 +439,10 @@ impl Server {
             .add_request_handler(forward_mutating_project_request::<proto::GitCreateWorktree>)
             .add_request_handler(disallow_guest_request::<proto::GitRemoveWorktree>)
             .add_request_handler(disallow_guest_request::<proto::GitRenameWorktree>)
+            .add_request_handler(forward_mutating_project_request::<proto::GitEditRef>)
+            .add_request_handler(forward_mutating_project_request::<proto::GitRepairWorktrees>)
+            .add_request_handler(disallow_guest_request::<proto::GitCreateArchiveCheckpoint>)
+            .add_request_handler(disallow_guest_request::<proto::GitRestoreArchiveCheckpoint>)
             .add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
             .add_request_handler(forward_mutating_project_request::<proto::ToggleLspLogs>)
             .add_message_handler(broadcast_project_message_from_host::<proto::LanguageServerLog>)

crates/fs/src/fake_git_repo.rs 🔗

@@ -10,8 +10,8 @@ use git::{
     repository::{
         AskPassDelegate, Branch, CommitDataReader, CommitDetails, CommitOptions,
         CreateWorktreeTarget, FetchOptions, GRAPH_CHUNK_SIZE, GitRepository,
-        GitRepositoryCheckpoint, InitialGraphCommitData, LogOrder, LogSource, PushOptions, Remote,
-        RepoPath, ResetMode, SearchCommitArgs, Worktree,
+        GitRepositoryCheckpoint, InitialGraphCommitData, LogOrder, LogSource, PushOptions, RefEdit,
+        Remote, RepoPath, ResetMode, SearchCommitArgs, Worktree,
     },
     stash::GitStash,
     status::{
@@ -109,6 +109,20 @@ impl FakeGitRepository {
         .boxed()
     }
 
+    fn edit_ref(&self, edit: RefEdit) -> BoxFuture<'_, Result<()>> {
+        self.with_state_async(true, move |state| {
+            match edit {
+                RefEdit::Update { ref_name, commit } => {
+                    state.refs.insert(ref_name, commit);
+                }
+                RefEdit::Delete { ref_name } => {
+                    state.refs.remove(&ref_name);
+                }
+            }
+            Ok(())
+        })
+    }
+
     /// Scans `.git/worktrees/*/gitdir` to find the admin entry directory for a
     /// worktree at the given checkout path. Used when the working tree directory
     /// has already been deleted and we can't read its `.git` pointer file.
@@ -1437,17 +1451,11 @@ impl GitRepository for FakeGitRepository {
     }
 
     fn update_ref(&self, ref_name: String, commit: String) -> BoxFuture<'_, Result<()>> {
-        self.with_state_async(true, move |state| {
-            state.refs.insert(ref_name, commit);
-            Ok(())
-        })
+        self.edit_ref(RefEdit::Update { ref_name, commit })
     }
 
     fn delete_ref(&self, ref_name: String) -> BoxFuture<'_, Result<()>> {
-        self.with_state_async(true, move |state| {
-            state.refs.remove(&ref_name);
-            Ok(())
-        })
+        self.edit_ref(RefEdit::Delete { ref_name })
     }
 
     fn repair_worktrees(&self) -> BoxFuture<'_, Result<()>> {

crates/git/src/repository.rs 🔗

@@ -1043,6 +1043,25 @@ pub struct RealGitRepository {
     is_trusted: Arc<AtomicBool>,
 }
 
+#[derive(Debug)]
+pub enum RefEdit {
+    Update { ref_name: String, commit: String },
+    Delete { ref_name: String },
+}
+
+impl RefEdit {
+    fn into_args(self) -> Vec<OsString> {
+        match self {
+            Self::Update { ref_name, commit } => {
+                vec!["update-ref".into(), ref_name.into(), commit.into()]
+            }
+            Self::Delete { ref_name } => {
+                vec!["update-ref".into(), "-d".into(), ref_name.into()]
+            }
+        }
+    }
+}
+
 impl RealGitRepository {
     pub fn new(
         dotgit_path: &Path,
@@ -1089,6 +1108,17 @@ impl RealGitRepository {
         ))
     }
 
+    fn edit_ref(&self, edit: RefEdit) -> BoxFuture<'_, Result<()>> {
+        let git_binary = self.git_binary();
+        self.executor
+            .spawn(async move {
+                let args = edit.into_args();
+                git_binary?.run(&args).await?;
+                Ok(())
+            })
+            .boxed()
+    }
+
     async fn any_git_binary_help_output(&self) -> SharedString {
         if let Some(output) = self.any_git_binary_help_output.lock().clone() {
             return output;
@@ -2316,25 +2346,11 @@ impl GitRepository for RealGitRepository {
     }
 
     fn update_ref(&self, ref_name: String, commit: String) -> BoxFuture<'_, Result<()>> {
-        let git_binary = self.git_binary();
-        self.executor
-            .spawn(async move {
-                let args: Vec<OsString> = vec!["update-ref".into(), ref_name.into(), commit.into()];
-                git_binary?.run(&args).await?;
-                Ok(())
-            })
-            .boxed()
+        self.edit_ref(RefEdit::Update { ref_name, commit })
     }
 
     fn delete_ref(&self, ref_name: String) -> BoxFuture<'_, Result<()>> {
-        let git_binary = self.git_binary();
-        self.executor
-            .spawn(async move {
-                let args: Vec<OsString> = vec!["update-ref".into(), "-d".into(), ref_name.into()];
-                git_binary?.run(&args).await?;
-                Ok(())
-            })
-            .boxed()
+        self.edit_ref(RefEdit::Delete { ref_name })
     }
 
     fn repair_worktrees(&self) -> BoxFuture<'_, Result<()>> {

crates/project/src/git_store.rs 🔗

@@ -563,7 +563,9 @@ impl GitStore {
         client.add_entity_request_handler(Self::handle_reset);
         client.add_entity_request_handler(Self::handle_show);
         client.add_entity_request_handler(Self::handle_create_checkpoint);
+        client.add_entity_request_handler(Self::handle_create_archive_checkpoint);
         client.add_entity_request_handler(Self::handle_restore_checkpoint);
+        client.add_entity_request_handler(Self::handle_restore_archive_checkpoint);
         client.add_entity_request_handler(Self::handle_compare_checkpoints);
         client.add_entity_request_handler(Self::handle_diff_checkpoints);
         client.add_entity_request_handler(Self::handle_load_commit_diff);
@@ -589,6 +591,8 @@ impl GitStore {
         client.add_entity_request_handler(Self::handle_remove_worktree);
         client.add_entity_request_handler(Self::handle_rename_worktree);
         client.add_entity_request_handler(Self::handle_get_head_sha);
+        client.add_entity_request_handler(Self::handle_edit_ref);
+        client.add_entity_request_handler(Self::handle_repair_worktrees);
     }
 
     pub fn is_local(&self) -> bool {
@@ -2519,6 +2523,46 @@ impl GitStore {
         Ok(proto::GitGetHeadShaResponse { sha: head_sha })
     }
 
+    async fn handle_edit_ref(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::GitEditRef>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
+        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
+        let ref_name = envelope.payload.ref_name;
+        let commit = match envelope.payload.action {
+            Some(proto::git_edit_ref::Action::UpdateToCommit(sha)) => Some(sha),
+            Some(proto::git_edit_ref::Action::Delete(_)) => None,
+            None => anyhow::bail!("GitEditRef missing action"),
+        };
+
+        repository_handle
+            .update(&mut cx, |repository_handle, _| {
+                repository_handle.edit_ref(ref_name, commit)
+            })
+            .await??;
+
+        Ok(proto::Ack {})
+    }
+
+    async fn handle_repair_worktrees(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::GitRepairWorktrees>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
+        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
+
+        repository_handle
+            .update(&mut cx, |repository_handle, _| {
+                repository_handle.repair_worktrees()
+            })
+            .await??;
+
+        Ok(proto::Ack {})
+    }
+
     async fn handle_get_branches(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::GitGetBranches>,
@@ -2705,6 +2749,26 @@ impl GitStore {
         })
     }
 
+    async fn handle_create_archive_checkpoint(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::GitCreateArchiveCheckpoint>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::GitCreateArchiveCheckpointResponse> {
+        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
+        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
+
+        let (staged_commit_sha, unstaged_commit_sha) = repository_handle
+            .update(&mut cx, |repository, _| {
+                repository.create_archive_checkpoint()
+            })
+            .await??;
+
+        Ok(proto::GitCreateArchiveCheckpointResponse {
+            staged_commit_sha,
+            unstaged_commit_sha,
+        })
+    }
+
     async fn handle_restore_checkpoint(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::GitRestoreCheckpoint>,
@@ -2726,6 +2790,25 @@ impl GitStore {
         Ok(proto::Ack {})
     }
 
+    async fn handle_restore_archive_checkpoint(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::GitRestoreArchiveCheckpoint>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
+        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
+        let staged_commit_sha = envelope.payload.staged_commit_sha;
+        let unstaged_commit_sha = envelope.payload.unstaged_commit_sha;
+
+        repository_handle
+            .update(&mut cx, |repository, _| {
+                repository.restore_archive_checkpoint(staged_commit_sha, unstaged_commit_sha)
+            })
+            .await??;
+
+        Ok(proto::Ack {})
+    }
+
     async fn handle_compare_checkpoints(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::GitCompareCheckpoints>,
@@ -6147,59 +6230,86 @@ impl Repository {
         })
     }
 
-    pub fn update_ref(
+    fn edit_ref(
         &mut self,
         ref_name: String,
-        commit: String,
+        commit: Option<String>,
     ) -> oneshot::Receiver<Result<()>> {
+        let id = self.id;
         self.send_job(None, move |repo, _cx| async move {
             match repo {
-                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
-                    backend.update_ref(ref_name, commit).await
-                }
-                RepositoryState::Remote(_) => {
-                    anyhow::bail!("update_ref is not supported for remote repositories")
+                RepositoryState::Local(LocalRepositoryState { backend, .. }) => match commit {
+                    Some(commit) => backend.update_ref(ref_name, commit).await,
+                    None => backend.delete_ref(ref_name).await,
+                },
+                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
+                    let action = match commit {
+                        Some(sha) => proto::git_edit_ref::Action::UpdateToCommit(sha),
+                        None => {
+                            proto::git_edit_ref::Action::Delete(proto::git_edit_ref::DeleteRef {})
+                        }
+                    };
+                    client
+                        .request(proto::GitEditRef {
+                            project_id: project_id.0,
+                            repository_id: id.to_proto(),
+                            ref_name,
+                            action: Some(action),
+                        })
+                        .await?;
+                    Ok(())
                 }
             }
         })
     }
 
+    pub fn update_ref(
+        &mut self,
+        ref_name: String,
+        commit: String,
+    ) -> oneshot::Receiver<Result<()>> {
+        self.edit_ref(ref_name, Some(commit))
+    }
+
     pub fn delete_ref(&mut self, ref_name: String) -> oneshot::Receiver<Result<()>> {
-        self.send_job(None, move |repo, _cx| async move {
-            match repo {
-                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
-                    backend.delete_ref(ref_name).await
-                }
-                RepositoryState::Remote(_) => {
-                    anyhow::bail!("delete_ref is not supported for remote repositories")
-                }
-            }
-        })
+        self.edit_ref(ref_name, None)
     }
 
     pub fn repair_worktrees(&mut self) -> oneshot::Receiver<Result<()>> {
+        let id = self.id;
         self.send_job(None, move |repo, _cx| async move {
             match repo {
                 RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
                     backend.repair_worktrees().await
                 }
-                RepositoryState::Remote(_) => {
-                    anyhow::bail!("repair_worktrees is not supported for remote repositories")
+                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
+                    client
+                        .request(proto::GitRepairWorktrees {
+                            project_id: project_id.0,
+                            repository_id: id.to_proto(),
+                        })
+                        .await?;
+                    Ok(())
                 }
             }
         })
     }
 
     pub fn create_archive_checkpoint(&mut self) -> oneshot::Receiver<Result<(String, String)>> {
+        let id = self.id;
         self.send_job(None, move |repo, _cx| async move {
             match repo {
                 RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
                     backend.create_archive_checkpoint().await
                 }
-                RepositoryState::Remote(_) => {
-                    anyhow::bail!(
-                        "create_archive_checkpoint is not supported for remote repositories"
-                    )
+                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
+                    let response = client
+                        .request(proto::GitCreateArchiveCheckpoint {
+                            project_id: project_id.0,
+                            repository_id: id.to_proto(),
+                        })
+                        .await?;
+                    Ok((response.staged_commit_sha, response.unstaged_commit_sha))
                 }
             }
         })
@@ -6210,6 +6320,7 @@ impl Repository {
         staged_sha: String,
         unstaged_sha: String,
     ) -> oneshot::Receiver<Result<()>> {
+        let id = self.id;
         self.send_job(None, move |repo, _cx| async move {
             match repo {
                 RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
@@ -6217,10 +6328,16 @@ impl Repository {
                         .restore_archive_checkpoint(staged_sha, unstaged_sha)
                         .await
                 }
-                RepositoryState::Remote(_) => {
-                    anyhow::bail!(
-                        "restore_archive_checkpoint is not supported for remote repositories"
-                    )
+                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
+                    client
+                        .request(proto::GitRestoreArchiveCheckpoint {
+                            project_id: project_id.0,
+                            repository_id: id.to_proto(),
+                            staged_commit_sha: staged_sha,
+                            unstaged_commit_sha: unstaged_sha,
+                        })
+                        .await?;
+                    Ok(())
                 }
             }
         })

crates/proto/proto/git.proto 🔗

@@ -577,6 +577,22 @@ message GitGetHeadShaResponse {
   optional string sha = 1;
 }
 
+message GitEditRef {
+  uint64 project_id = 1;
+  uint64 repository_id = 2;
+  string ref_name = 3;
+  oneof action {
+    string update_to_commit = 4;
+    DeleteRef delete = 5;
+  }
+  message DeleteRef {}
+}
+
+message GitRepairWorktrees {
+  uint64 project_id = 1;
+  uint64 repository_id = 2;
+}
+
 message GitWorktreesResponse {
   repeated Worktree worktrees = 1;
 }
@@ -607,12 +623,29 @@ message GitCreateCheckpointResponse {
   bytes commit_sha = 1;
 }
 
+message GitCreateArchiveCheckpoint {
+  uint64 project_id = 1;
+  uint64 repository_id = 2;
+}
+
+message GitCreateArchiveCheckpointResponse {
+  string staged_commit_sha = 1;
+  string unstaged_commit_sha = 2;
+}
+
 message GitRestoreCheckpoint {
   uint64 project_id = 1;
   uint64 repository_id = 2;
   bytes commit_sha = 3;
 }
 
+message GitRestoreArchiveCheckpoint {
+  uint64 project_id = 1;
+  uint64 repository_id = 2;
+  string staged_commit_sha = 3;
+  string unstaged_commit_sha = 4;
+}
+
 message GitCompareCheckpoints {
   uint64 project_id = 1;
   uint64 repository_id = 2;

crates/proto/proto/zed.proto 🔗

@@ -476,7 +476,12 @@ message Envelope {
     GitDiffCheckpoints git_diff_checkpoints = 438;
     GitDiffCheckpointsResponse git_diff_checkpoints_response = 439;
     GitGetHeadSha git_get_head_sha = 440;
-    GitGetHeadShaResponse git_get_head_sha_response = 441; // current max
+    GitGetHeadShaResponse git_get_head_sha_response = 441;
+    GitRepairWorktrees git_repair_worktrees = 442;
+    GitEditRef git_edit_ref = 443;
+    GitCreateArchiveCheckpoint git_create_archive_checkpoint = 444;
+    GitCreateArchiveCheckpointResponse git_create_archive_checkpoint_response = 445;
+    GitRestoreArchiveCheckpoint git_restore_archive_checkpoint = 446; // current max
   }
 
   reserved 87 to 88;

crates/proto/src/proto.rs 🔗

@@ -296,7 +296,10 @@ messages!(
     (GitFileHistoryResponse, Background),
     (GitCreateCheckpoint, Background),
     (GitCreateCheckpointResponse, Background),
+    (GitCreateArchiveCheckpoint, Background),
+    (GitCreateArchiveCheckpointResponse, Background),
     (GitRestoreCheckpoint, Background),
+    (GitRestoreArchiveCheckpoint, Background),
     (GitCompareCheckpoints, Background),
     (GitCompareCheckpointsResponse, Background),
     (GitDiffCheckpoints, Background),
@@ -353,6 +356,8 @@ messages!(
     (GitGetWorktrees, Background),
     (GitGetHeadSha, Background),
     (GitGetHeadShaResponse, Background),
+    (GitEditRef, Background),
+    (GitRepairWorktrees, Background),
     (GitWorktreesResponse, Background),
     (GitCreateWorktree, Background),
     (GitRemoveWorktree, Background),
@@ -524,7 +529,12 @@ request_messages!(
     (GitShow, GitCommitDetails),
     (GitFileHistory, GitFileHistoryResponse),
     (GitCreateCheckpoint, GitCreateCheckpointResponse),
+    (
+        GitCreateArchiveCheckpoint,
+        GitCreateArchiveCheckpointResponse
+    ),
     (GitRestoreCheckpoint, Ack),
+    (GitRestoreArchiveCheckpoint, Ack),
     (GitCompareCheckpoints, GitCompareCheckpointsResponse),
     (GitDiffCheckpoints, GitDiffCheckpointsResponse),
     (GitReset, Ack),
@@ -561,6 +571,8 @@ request_messages!(
     (RemoteStarted, Ack),
     (GitGetWorktrees, GitWorktreesResponse),
     (GitGetHeadSha, GitGetHeadShaResponse),
+    (GitEditRef, Ack),
+    (GitRepairWorktrees, Ack),
     (GitCreateWorktree, Ack),
     (GitRemoveWorktree, Ack),
     (GitRenameWorktree, Ack),
@@ -753,6 +765,10 @@ entity_messages!(
     NewExternalAgentVersionAvailable,
     GitGetWorktrees,
     GitGetHeadSha,
+    GitEditRef,
+    GitRepairWorktrees,
+    GitCreateArchiveCheckpoint,
+    GitRestoreArchiveCheckpoint,
     GitCreateWorktree,
     GitRemoveWorktree,
     GitRenameWorktree,

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -1622,6 +1622,98 @@ async fn test_remote_root_repo_common_dir(cx: &mut TestAppContext, server_cx: &m
     assert_eq!(common_dir, None);
 }
 
+#[gpui::test]
+async fn test_remote_archive_git_operations_are_supported(
+    cx: &mut TestAppContext,
+    server_cx: &mut TestAppContext,
+) {
+    let fs = FakeFs::new(server_cx.executor());
+    fs.insert_tree(
+        "/project",
+        json!({
+            ".git": {},
+            "file.txt": "content",
+        }),
+    )
+    .await;
+    fs.set_branch_name(Path::new("/project/.git"), Some("main"));
+    fs.set_head_for_repo(
+        Path::new("/project/.git"),
+        &[("file.txt", "content".into())],
+        "head-sha",
+    );
+
+    let (project, _headless) = init_test(&fs, cx, server_cx).await;
+    project
+        .update(cx, |project, cx| {
+            project.find_or_create_worktree(Path::new("/project"), true, cx)
+        })
+        .await
+        .expect("should open remote worktree");
+    cx.run_until_parked();
+
+    let repository = project.read_with(cx, |project, cx| {
+        project
+            .active_repository(cx)
+            .expect("remote project should have an active repository")
+    });
+
+    let head_sha = cx
+        .update(|cx| repository.update(cx, |repository, _| repository.head_sha()))
+        .await
+        .expect("head_sha request should complete")
+        .expect("head_sha should succeed")
+        .expect("HEAD should exist");
+
+    cx.run_until_parked();
+
+    cx.update(|cx| {
+        repository.update(cx, |repository, _| {
+            repository.update_ref("refs/zed-tests/archive-checkpoint".to_string(), head_sha)
+        })
+    })
+    .await
+    .expect("update_ref request should complete")
+    .expect("update_ref should succeed for remote repository");
+
+    cx.run_until_parked();
+
+    cx.update(|cx| {
+        repository.update(cx, |repository, _| {
+            repository.delete_ref("refs/zed-tests/archive-checkpoint".to_string())
+        })
+    })
+    .await
+    .expect("delete_ref request should complete")
+    .expect("delete_ref should succeed for remote repository");
+
+    cx.run_until_parked();
+
+    cx.update(|cx| repository.update(cx, |repository, _| repository.repair_worktrees()))
+        .await
+        .expect("repair_worktrees request should complete")
+        .expect("repair_worktrees should succeed for remote repository");
+
+    cx.run_until_parked();
+
+    let (staged_commit_sha, unstaged_commit_sha) = cx
+        .update(|cx| repository.update(cx, |repository, _| repository.create_archive_checkpoint()))
+        .await
+        .expect("create_archive_checkpoint request should complete")
+        .expect("create_archive_checkpoint should succeed for remote repository");
+
+    cx.run_until_parked();
+
+    cx.update(|cx| {
+        repository.update(cx, |repository, _| {
+            repository.restore_archive_checkpoint(staged_commit_sha, unstaged_commit_sha)
+        })
+    })
+    .await
+    .expect("restore_archive_checkpoint request should complete")
+    .expect("restore_archive_checkpoint should succeed for remote repository");
+}
+
 #[gpui::test]
 async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
     let text_2 = "