diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fa84a95837d390e4c81c09c1e11d7fc4ad704f20..986330e118de9eaf4abff99357d7d865ba302a94 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -439,6 +439,10 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(disallow_guest_request::) .add_request_handler(disallow_guest_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(disallow_guest_request::) + .add_request_handler(disallow_guest_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_message_handler(broadcast_project_message_from_host::) diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 2b84de531e081f210208b09b2c3cb28ce7c3c666..8498652d5ee62557f4a7a9519fce34b0b2ccc15b 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/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<()>> { diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index cb8172bab507818a9f06dc1d84afd0e9f11477c6..3215d761e0ace95d9010bbcb9ac7bb0b711c2c82 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1043,6 +1043,25 @@ pub struct RealGitRepository { is_trusted: Arc, } +#[derive(Debug)] +pub enum RefEdit { + Update { ref_name: String, commit: String }, + Delete { ref_name: String }, +} + +impl RefEdit { + fn into_args(self) -> Vec { + 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 = 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 = 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<()>> { diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index fe7da2e3f5c763659e183cc02627f3a59780fa14..592fc8daf48d523a1f2805dede6f07013f65082a 100644 --- a/crates/project/src/git_store.rs +++ b/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, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + 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, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + 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, envelope: TypedEnvelope, @@ -2705,6 +2749,26 @@ impl GitStore { }) } + async fn handle_create_archive_checkpoint( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + 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, envelope: TypedEnvelope, @@ -2726,6 +2790,25 @@ impl GitStore { Ok(proto::Ack {}) } + async fn handle_restore_archive_checkpoint( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + 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, envelope: TypedEnvelope, @@ -6147,59 +6230,86 @@ impl Repository { }) } - pub fn update_ref( + fn edit_ref( &mut self, ref_name: String, - commit: String, + commit: Option, ) -> oneshot::Receiver> { + 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> { + self.edit_ref(ref_name, Some(commit)) + } + pub fn delete_ref(&mut self, ref_name: String) -> oneshot::Receiver> { - 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> { + 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> { + 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> { + 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(()) } } }) diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index d7c0d9bb9ac8c1b661d5306fe9f01336da7e5970..78f3fb2aea9dec9f18a2086c0d7599f729dae174 100644 --- a/crates/proto/proto/git.proto +++ b/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; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 8b62754d7af40b7c4f5e1a87ad42899d682ba453..1da7b96892263385f13df19f6208898cfe090266 100644 --- a/crates/proto/proto/zed.proto +++ b/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; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index b77bd02313c13a9b04eb7762a97f9e77ac8cbaf8..83a559cb28330601424d3d4f2d2efc6191b3ebb9 100644 --- a/crates/proto/src/proto.rs +++ b/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, diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index d9d737bcd4b7f7c64ce69b995231c7ca5f751d24..c8876ed2328eb3946a80126e710ca7af29483ffb 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/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 = "