Implement collaborative git manipulations (#23869)

Now commit, stage and unstage can be done both via remote ssh and via
collab (by guests with write access).



https://github.com/user-attachments/assets/a0f5e4e8-01a3-402b-a1f7-f3fc1236cffd


Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2025-01-30 11:23:38 +02:00 committed by GitHub
parent e721dac367
commit 41de83fe1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 482 additions and 30 deletions

View file

@ -1,6 +1,7 @@
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
use crate::{Project, ProjectPath};
use anyhow::anyhow;
use anyhow::{anyhow, Context as _};
use client::ProjectId;
use futures::channel::mpsc;
use futures::{SinkExt as _, StreamExt as _};
use git::{
@ -11,13 +12,16 @@ use gpui::{
App, AppContext as _, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity,
};
use language::{Buffer, LanguageRegistry};
use rpc::{proto, AnyProtoClient};
use settings::WorktreeId;
use std::sync::Arc;
use text::Rope;
use util::maybe;
use worktree::{RepositoryEntry, StatusEntry};
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
pub struct GitState {
project_id: Option<ProjectId>,
client: Option<AnyProtoClient>,
repositories: Vec<RepositoryHandle>,
active_index: Option<usize>,
update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
@ -28,13 +32,24 @@ pub struct GitState {
#[derive(Clone)]
pub struct RepositoryHandle {
git_state: WeakEntity<GitState>,
worktree_id: WorktreeId,
repository_entry: RepositoryEntry,
git_repo: Option<Arc<dyn GitRepository>>,
pub worktree_id: WorktreeId,
pub repository_entry: RepositoryEntry,
git_repo: Option<GitRepo>,
commit_message: Entity<Buffer>,
update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
}
#[derive(Clone)]
enum GitRepo {
Local(Arc<dyn GitRepository>),
Remote {
project_id: ProjectId,
client: AnyProtoClient,
worktree_id: WorktreeId,
work_directory_id: ProjectEntryId,
},
}
impl PartialEq<Self> for RepositoryHandle {
fn eq(&self, other: &Self) -> bool {
self.worktree_id == other.worktree_id
@ -52,10 +67,10 @@ impl PartialEq<RepositoryEntry> for RepositoryHandle {
}
enum Message {
StageAndCommit(Arc<dyn GitRepository>, Rope, Vec<RepoPath>),
Commit(Arc<dyn GitRepository>, Rope),
Stage(Arc<dyn GitRepository>, Vec<RepoPath>),
Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
StageAndCommit(GitRepo, Rope, Vec<RepoPath>),
Commit(GitRepo, Rope),
Stage(GitRepo, Vec<RepoPath>),
Unstage(GitRepo, Vec<RepoPath>),
}
pub enum Event {
@ -68,6 +83,8 @@ impl GitState {
pub fn new(
worktree_store: &Entity<WorktreeStore>,
languages: Arc<LanguageRegistry>,
client: Option<AnyProtoClient>,
project_id: Option<ProjectId>,
cx: &mut Context<'_, Self>,
) -> Self {
let (update_sender, mut update_receiver) =
@ -79,13 +96,117 @@ impl GitState {
.spawn(async move {
match msg {
Message::StageAndCommit(repo, message, paths) => {
repo.stage_paths(&paths)?;
repo.commit(&message.to_string())?;
match repo {
GitRepo::Local(repo) => {
repo.stage_paths(&paths)?;
repo.commit(&message.to_string())?;
}
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Stage {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
paths: paths
.into_iter()
.map(|repo_path| repo_path.to_proto())
.collect(),
})
.await
.context("sending stage request")?;
client
.request(proto::Commit {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
message: message.to_string(),
})
.await
.context("sending commit request")?;
}
}
Ok(())
}
Message::Stage(repo, paths) => {
match repo {
GitRepo::Local(repo) => repo.stage_paths(&paths)?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Stage {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
paths: paths
.into_iter()
.map(|repo_path| repo_path.to_proto())
.collect(),
})
.await
.context("sending stage request")?;
}
}
Ok(())
}
Message::Unstage(repo, paths) => {
match repo {
GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Unstage {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
paths: paths
.into_iter()
.map(|repo_path| repo_path.to_proto())
.collect(),
})
.await
.context("sending unstage request")?;
}
}
Ok(())
}
Message::Commit(repo, message) => {
match repo {
GitRepo::Local(repo) => repo.commit(&message.to_string())?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Commit {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
// TODO implement collaborative commit message buffer instead and use it
// If it works, remove `commit_with_message` method.
message: message.to_string(),
})
.await
.context("sending commit request")?;
}
}
Ok(())
}
Message::Stage(repo, paths) => repo.stage_paths(&paths),
Message::Unstage(repo, paths) => repo.unstage_paths(&paths),
Message::Commit(repo, message) => repo.commit(&message.to_string()),
}
})
.await;
@ -99,7 +220,9 @@ impl GitState {
let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
GitState {
project_id,
languages,
client,
repositories: Vec::new(),
active_index: None,
update_sender,
@ -123,6 +246,8 @@ impl GitState {
let mut new_repositories = Vec::new();
let mut new_active_index = None;
let this = cx.weak_entity();
let client = self.client.clone();
let project_id = self.project_id;
worktree_store.update(cx, |worktree_store, cx| {
for worktree in worktree_store.worktrees() {
@ -132,7 +257,18 @@ impl GitState {
let git_repo = worktree
.as_local()
.and_then(|local_worktree| local_worktree.get_local_repo(repo))
.map(|local_repo| local_repo.repo().clone());
.map(|local_repo| local_repo.repo().clone())
.map(GitRepo::Local)
.or_else(|| {
let client = client.clone()?;
let project_id = project_id?;
Some(GitRepo::Remote {
project_id,
client,
worktree_id: worktree.id(),
work_directory_id: repo.work_directory_id(),
})
});
let existing = self
.repositories
.iter()
@ -340,6 +476,21 @@ impl RepositoryHandle {
});
}
pub fn commit_with_message(
&self,
message: String,
err_sender: mpsc::Sender<anyhow::Error>,
) -> anyhow::Result<()> {
let Some(git_repo) = self.git_repo.clone() else {
return Ok(());
};
let result = self
.update_sender
.unbounded_send((Message::Commit(git_repo, message.into()), err_sender));
anyhow::ensure!(result.is_ok(), "Failed to submit commit operation");
Ok(())
}
pub fn commit_all(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
let Some(git_repo) = self.git_repo.clone() else {
return;