Skip to content

Commit

Permalink
Implement collaborative git manipulations (#23869)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
SomeoneToIgnore authored Jan 30, 2025
1 parent e721dac commit 41de83f
Show file tree
Hide file tree
Showing 7 changed files with 482 additions and 30 deletions.
3 changes: 3 additions & 0 deletions crates/collab/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,9 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
.add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
.add_message_handler(update_context)
.add_request_handler({
Expand Down
181 changes: 166 additions & 15 deletions crates/project/src/git.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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>)>,
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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) =
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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() {
Expand All @@ -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()
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 41de83f

Please sign in to comment.