Detailed changes
@@ -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>)
@@ -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<()>> {
@@ -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<()>> {
@@ -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(())
}
}
})
@@ -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;
@@ -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;
@@ -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,
@@ -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 = "