Add support for git branches on remote projects (#19755)

Release Notes:

- Fixed a bug where the branch switcher could not be used remotely.
This commit is contained in:
Mikayla Maki 2024-10-27 15:50:54 -07:00 committed by GitHub
parent 5506669b06
commit c69da2df70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 993 additions and 127 deletions

View file

@ -288,6 +288,13 @@ impl ProjectPath {
path: self.path.to_string_lossy().to_string(),
}
}
pub fn root_path(worktree_id: WorktreeId) -> Self {
Self {
worktree_id,
path: Path::new("").into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -701,7 +708,7 @@ impl Project {
let ssh_proto = ssh.read(cx).proto_client();
let worktree_store =
cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), 0));
cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID));
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
@ -3370,6 +3377,25 @@ impl Project {
worktree.get_local_repo(&root_entry)?.repo().clone().into()
}
pub fn branches(
&self,
project_path: ProjectPath,
cx: &AppContext,
) -> Task<Result<Vec<git::repository::Branch>>> {
self.worktree_store().read(cx).branches(project_path, cx)
}
pub fn update_or_create_branch(
&self,
repository: ProjectPath,
new_branch: String,
cx: &AppContext,
) -> Task<Result<()>> {
self.worktree_store()
.read(cx)
.update_or_create_branch(repository, new_branch, cx)
}
pub fn blame_buffer(
&self,
buffer: &Model<Buffer>,

View file

@ -73,6 +73,8 @@ impl WorktreeStore {
client.add_model_request_handler(Self::handle_copy_project_entry);
client.add_model_request_handler(Self::handle_delete_project_entry);
client.add_model_request_handler(Self::handle_expand_project_entry);
client.add_model_request_handler(Self::handle_git_branches);
client.add_model_request_handler(Self::handle_update_branch);
}
pub fn local(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
@ -127,6 +129,13 @@ impl WorktreeStore {
.find(|worktree| worktree.read(cx).id() == id)
}
pub fn current_branch(&self, repository: ProjectPath, cx: &AppContext) -> Option<Arc<str>> {
self.worktree_for_id(repository.worktree_id, cx)?
.read(cx)
.git_entry(repository.path)?
.branch()
}
pub fn worktree_for_entry(
&self,
entry_id: ProjectEntryId,
@ -836,6 +845,131 @@ impl WorktreeStore {
Ok(())
}
pub fn branches(
&self,
project_path: ProjectPath,
cx: &AppContext,
) -> Task<Result<Vec<git::repository::Branch>>> {
let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
};
match worktree.read(cx) {
Worktree::Local(local_worktree) => {
let branches = util::maybe!({
let worktree_error = |error| {
format!(
"{} for worktree {}",
error,
local_worktree.abs_path().to_string_lossy()
)
};
let entry = local_worktree
.git_entry(project_path.path)
.with_context(|| worktree_error("No git entry found"))?;
let repo = local_worktree
.get_local_repo(&entry)
.with_context(|| worktree_error("No repository found"))?
.repo()
.clone();
repo.branches()
});
Task::ready(branches)
}
Worktree::Remote(remote_worktree) => {
let request = remote_worktree.client().request(proto::GitBranches {
project_id: remote_worktree.project_id(),
repository: Some(proto::ProjectPath {
worktree_id: project_path.worktree_id.to_proto(),
path: project_path.path.to_string_lossy().to_string(), // Root path
}),
});
cx.background_executor().spawn(async move {
let response = request.await?;
let branches = response
.branches
.into_iter()
.map(|proto_branch| git::repository::Branch {
is_head: proto_branch.is_head,
name: proto_branch.name.into(),
unix_timestamp: proto_branch
.unix_timestamp
.map(|timestamp| timestamp as i64),
})
.collect();
Ok(branches)
})
}
}
}
pub fn update_or_create_branch(
&self,
repository: ProjectPath,
new_branch: String,
cx: &AppContext,
) -> Task<Result<()>> {
let Some(worktree) = self.worktree_for_id(repository.worktree_id, cx) else {
return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
};
match worktree.read(cx) {
Worktree::Local(local_worktree) => {
let result = util::maybe!({
let worktree_error = |error| {
format!(
"{} for worktree {}",
error,
local_worktree.abs_path().to_string_lossy()
)
};
let entry = local_worktree
.git_entry(repository.path)
.with_context(|| worktree_error("No git entry found"))?;
let repo = local_worktree
.get_local_repo(&entry)
.with_context(|| worktree_error("No repository found"))?
.repo()
.clone();
if !repo.branch_exits(&new_branch)? {
repo.create_branch(&new_branch)?;
}
repo.change_branch(&new_branch)?;
Ok(())
});
Task::ready(result)
}
Worktree::Remote(remote_worktree) => {
let request = remote_worktree.client().request(proto::UpdateGitBranch {
project_id: remote_worktree.project_id(),
repository: Some(proto::ProjectPath {
worktree_id: repository.worktree_id.to_proto(),
path: repository.path.to_string_lossy().to_string(), // Root path
}),
branch_name: new_branch,
});
cx.background_executor().spawn(async move {
request.await?;
Ok(())
})
}
}
}
async fn filter_paths(
fs: &Arc<dyn Fs>,
mut input: Receiver<MatchingEntry>,
@ -917,6 +1051,61 @@ impl WorktreeStore {
.ok_or_else(|| anyhow!("invalid request"))?;
Worktree::handle_expand_entry(worktree, envelope.payload, cx).await
}
pub async fn handle_git_branches(
this: Model<Self>,
branches: TypedEnvelope<proto::GitBranches>,
cx: AsyncAppContext,
) -> Result<proto::GitBranchesResponse> {
let project_path = branches
.payload
.repository
.clone()
.context("Invalid GitBranches call")?;
let project_path = ProjectPath {
worktree_id: WorktreeId::from_proto(project_path.worktree_id),
path: Path::new(&project_path.path).into(),
};
let branches = this
.read_with(&cx, |this, cx| this.branches(project_path, cx))?
.await?;
Ok(proto::GitBranchesResponse {
branches: branches
.into_iter()
.map(|branch| proto::Branch {
is_head: branch.is_head,
name: branch.name.to_string(),
unix_timestamp: branch.unix_timestamp.map(|timestamp| timestamp as u64),
})
.collect(),
})
}
pub async fn handle_update_branch(
this: Model<Self>,
update_branch: TypedEnvelope<proto::UpdateGitBranch>,
cx: AsyncAppContext,
) -> Result<proto::Ack> {
let project_path = update_branch
.payload
.repository
.clone()
.context("Invalid GitBranches call")?;
let project_path = ProjectPath {
worktree_id: WorktreeId::from_proto(project_path.worktree_id),
path: Path::new(&project_path.path).into(),
};
let new_branch = update_branch.payload.branch_name;
this.read_with(&cx, |this, cx| {
this.update_or_create_branch(project_path, new_branch, cx)
})?
.await?;
Ok(proto::Ack {})
}
}
#[derive(Clone, Debug)]