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:
parent
e721dac367
commit
41de83fe1f
7 changed files with 482 additions and 30 deletions
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue