hosted projects (#8627)

- **Allow joining a hosted project**

You can't yet do anything in a hosted project, but you can join it and
look how empty it is.

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2024-03-04 19:17:40 -07:00 committed by GitHub
parent 4167c66b86
commit 27c5343707
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 519 additions and 232 deletions

View File

@ -1182,19 +1182,10 @@ impl Room {
) -> Task<Result<Model<Project>>> { ) -> Task<Result<Model<Project>>> {
let client = self.client.clone(); let client = self.client.clone();
let user_store = self.user_store.clone(); let user_store = self.user_store.clone();
let role = self.local_participant.role;
cx.emit(Event::RemoteProjectJoined { project_id: id }); cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
let project = Project::remote( let project =
id, Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
client,
user_store,
language_registry,
fs,
role,
cx.clone(),
)
.await?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.joined_projects.retain(|project| { this.joined_projects.retain(|project| {

View File

@ -11,7 +11,7 @@ pub use channel_chat::{
mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
MessageParams, MessageParams,
}; };
pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore, HostedProjectId}; pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore};
#[cfg(test)] #[cfg(test)]
mod channel_store_tests; mod channel_store_tests;

View File

@ -3,7 +3,9 @@ mod channel_index;
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage}; use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use channel_index::ChannelIndex; use channel_index::ChannelIndex;
use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore}; use client::{
ChannelId, Client, ClientSettings, HostedProjectId, Subscription, User, UserId, UserStore,
};
use collections::{hash_map, HashMap, HashSet}; use collections::{hash_map, HashMap, HashSet};
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{ use gpui::{
@ -27,9 +29,6 @@ pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppCont
cx.set_global(GlobalChannelStore(channel_store)); cx.set_global(GlobalChannelStore(channel_store));
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct HostedProjectId(pub u64);
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
struct NotesVersion { struct NotesVersion {
epoch: u64, epoch: u64,

View File

@ -24,6 +24,9 @@ impl std::fmt::Display for ChannelId {
} }
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct HostedProjectId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParticipantIndex(pub u32); pub struct ParticipantIndex(pub u32);

View File

@ -46,10 +46,11 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
CREATE TABLE "projects" ( CREATE TABLE "projects" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT, "id" INTEGER PRIMARY KEY AUTOINCREMENT,
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL, "room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
"host_user_id" INTEGER REFERENCES users (id) NOT NULL, "host_user_id" INTEGER REFERENCES users (id),
"host_connection_id" INTEGER, "host_connection_id" INTEGER,
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE, "host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE "unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
"hosted_project_id" INTEGER REFERENCES hosted_projects (id)
); );
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id"); CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id"); CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");

View File

@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE projects ALTER COLUMN host_user_id DROP NOT NULL;
ALTER TABLE projects ADD COLUMN hosted_project_id INTEGER REFERENCES hosted_projects(id) UNIQUE NULL;

View File

@ -670,6 +670,8 @@ pub struct RefreshedChannelBuffer {
} }
pub struct Project { pub struct Project {
pub id: ProjectId,
pub role: ChannelRole,
pub collaborators: Vec<ProjectCollaborator>, pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: BTreeMap<u64, Worktree>, pub worktrees: BTreeMap<u64, Worktree>,
pub language_servers: Vec<proto::LanguageServer>, pub language_servers: Vec<proto::LanguageServer>,
@ -695,7 +697,7 @@ impl ProjectCollaborator {
#[derive(Debug)] #[derive(Debug)]
pub struct LeftProject { pub struct LeftProject {
pub id: ProjectId, pub id: ProjectId,
pub host_user_id: UserId, pub host_user_id: Option<UserId>,
pub host_connection_id: Option<ConnectionId>, pub host_connection_id: Option<ConnectionId>,
pub connection_ids: Vec<ConnectionId>, pub connection_ids: Vec<ConnectionId>,
} }

View File

@ -1,4 +1,4 @@
use rpc::proto; use rpc::{proto, ErrorCode};
use super::*; use super::*;
@ -39,4 +39,44 @@ impl Database {
}) })
.collect()) .collect())
} }
pub async fn get_hosted_project(
&self,
hosted_project_id: HostedProjectId,
user_id: UserId,
tx: &DatabaseTransaction,
) -> Result<(hosted_project::Model, ChannelRole)> {
let project = hosted_project::Entity::find_by_id(hosted_project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?;
let channel = channel::Entity::find_by_id(project.channel_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!(ErrorCode::NoSuchChannel))?;
let role = match project.visibility {
ChannelVisibility::Public => {
self.check_user_is_channel_participant(&channel, user_id, tx)
.await?
}
ChannelVisibility::Members => {
self.check_user_is_channel_member(&channel, user_id, tx)
.await?
}
};
Ok((project, role))
}
pub async fn is_hosted_project(&self, project_id: ProjectId) -> Result<bool> {
self.transaction(|tx| async move {
Ok(project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.map(|project| project.hosted_project_id.is_some())
.ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?)
})
.await
}
} }

View File

@ -57,13 +57,14 @@ impl Database {
} }
let project = project::ActiveModel { let project = project::ActiveModel {
room_id: ActiveValue::set(participant.room_id), room_id: ActiveValue::set(Some(participant.room_id)),
host_user_id: ActiveValue::set(participant.user_id), host_user_id: ActiveValue::set(Some(participant.user_id)),
host_connection_id: ActiveValue::set(Some(connection.id as i32)), host_connection_id: ActiveValue::set(Some(connection.id as i32)),
host_connection_server_id: ActiveValue::set(Some(ServerId( host_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32, connection.owner_id as i32,
))), ))),
..Default::default() id: ActiveValue::NotSet,
hosted_project_id: ActiveValue::Set(None),
} }
.insert(&*tx) .insert(&*tx)
.await?; .await?;
@ -153,8 +154,12 @@ impl Database {
self.update_project_worktrees(project.id, worktrees, &tx) self.update_project_worktrees(project.id, worktrees, &tx)
.await?; .await?;
let room_id = project
.room_id
.ok_or_else(|| anyhow!("project not in a room"))?;
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?; let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
let room = self.get_room(project.room_id, &tx).await?; let room = self.get_room(room_id, &tx).await?;
Ok((room, guest_connection_ids)) Ok((room, guest_connection_ids))
}) })
.await .await
@ -504,8 +509,30 @@ impl Database {
.await .await
} }
/// Adds the given connection to the specified project. /// Adds the given connection to the specified hosted project
pub async fn join_project( pub async fn join_hosted_project(
&self,
id: HostedProjectId,
user_id: UserId,
connection: ConnectionId,
) -> Result<(Project, ReplicaId)> {
self.transaction(|tx| async move {
let (hosted_project, role) = self.get_hosted_project(id, user_id, &tx).await?;
let project = project::Entity::find()
.filter(project::Column::HostedProjectId.eq(hosted_project.id))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("hosted project is no longer shared"))?;
self.join_project_internal(project, user_id, connection, role, &tx)
.await
})
.await
}
/// Adds the given connection to the specified project
/// in the current room.
pub async fn join_project_in_room(
&self, &self,
project_id: ProjectId, project_id: ProjectId,
connection: ConnectionId, connection: ConnectionId,
@ -532,180 +559,240 @@ impl Database {
.one(&*tx) .one(&*tx)
.await? .await?
.ok_or_else(|| anyhow!("no such project"))?; .ok_or_else(|| anyhow!("no such project"))?;
if project.room_id != participant.room_id { if project.room_id != Some(participant.room_id) {
return Err(anyhow!("no such project"))?; return Err(anyhow!("no such project"))?;
} }
self.join_project_internal(
project,
participant.user_id,
connection,
participant.role.unwrap_or(ChannelRole::Member),
&tx,
)
.await
})
.await
}
let mut collaborators = project async fn join_project_internal(
&self,
project: project::Model,
user_id: UserId,
connection: ConnectionId,
role: ChannelRole,
tx: &DatabaseTransaction,
) -> Result<(Project, ReplicaId)> {
let mut collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
let replica_ids = collaborators
.iter()
.map(|c| c.replica_id)
.collect::<HashSet<_>>();
let mut replica_id = ReplicaId(1);
while replica_ids.contains(&replica_id) {
replica_id.0 += 1;
}
let new_collaborator = project_collaborator::ActiveModel {
project_id: ActiveValue::set(project.id),
connection_id: ActiveValue::set(connection.id as i32),
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
user_id: ActiveValue::set(user_id),
replica_id: ActiveValue::set(replica_id),
is_host: ActiveValue::set(false),
..Default::default()
}
.insert(&*tx)
.await?;
collaborators.push(new_collaborator);
let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
let mut worktrees = db_worktrees
.into_iter()
.map(|db_worktree| {
(
db_worktree.id as u64,
Worktree {
id: db_worktree.id as u64,
abs_path: db_worktree.abs_path,
root_name: db_worktree.root_name,
visible: db_worktree.visible,
entries: Default::default(),
repository_entries: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
},
)
})
.collect::<BTreeMap<_, _>>();
// Populate worktree entries.
{
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
.add(worktree_entry::Column::ProjectId.eq(project.id))
.add(worktree_entry::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
let db_entry = db_entry?;
if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) {
worktree.entries.push(proto::Entry {
id: db_entry.id as u64,
is_dir: db_entry.is_dir,
path: db_entry.path,
inode: db_entry.inode as u64,
mtime: Some(proto::Timestamp {
seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32,
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
git_status: db_entry.git_status.map(|status| status as i32),
});
}
}
}
// Populate repository entries.
{
let mut db_repository_entries = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_repository_entry) = db_repository_entries.next().await {
let db_repository_entry = db_repository_entry?;
if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
{
worktree.repository_entries.insert(
db_repository_entry.work_directory_id as u64,
proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
},
);
}
}
}
// Populate worktree diagnostic summaries.
{
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
.filter(worktree_diagnostic_summary::Column::ProjectId.eq(project.id))
.stream(&*tx)
.await?;
while let Some(db_summary) = db_summaries.next().await {
let db_summary = db_summary?;
if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) {
worktree
.diagnostic_summaries
.push(proto::DiagnosticSummary {
path: db_summary.path,
language_server_id: db_summary.language_server_id as u64,
error_count: db_summary.error_count as u32,
warning_count: db_summary.warning_count as u32,
});
}
}
}
// Populate worktree settings files
{
let mut db_settings_files = worktree_settings_file::Entity::find()
.filter(worktree_settings_file::Column::ProjectId.eq(project.id))
.stream(&*tx)
.await?;
while let Some(db_settings_file) = db_settings_files.next().await {
let db_settings_file = db_settings_file?;
if let Some(worktree) = worktrees.get_mut(&(db_settings_file.worktree_id as u64)) {
worktree.settings_files.push(WorktreeSettingsFile {
path: db_settings_file.path,
content: db_settings_file.content,
});
}
}
}
// Populate language servers.
let language_servers = project
.find_related(language_server::Entity)
.all(&*tx)
.await?;
let project = Project {
id: project.id,
role,
collaborators: collaborators
.into_iter()
.map(|collaborator| ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect(),
worktrees,
language_servers: language_servers
.into_iter()
.map(|language_server| proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
})
.collect(),
};
Ok((project, replica_id as ReplicaId))
}
pub async fn leave_hosted_project(
&self,
project_id: ProjectId,
connection: ConnectionId,
) -> Result<LeftProject> {
self.transaction(|tx| async move {
let result = project_collaborator::Entity::delete_many()
.filter(
Condition::all()
.add(project_collaborator::Column::ProjectId.eq(project_id))
.add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection.owner_id as i32),
),
)
.exec(&*tx)
.await?;
if result.rows_affected == 0 {
return Err(anyhow!("not in the project"))?;
}
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let collaborators = project
.find_related(project_collaborator::Entity) .find_related(project_collaborator::Entity)
.all(&*tx) .all(&*tx)
.await?; .await?;
let replica_ids = collaborators let connection_ids = collaborators
.iter()
.map(|c| c.replica_id)
.collect::<HashSet<_>>();
let mut replica_id = ReplicaId(1);
while replica_ids.contains(&replica_id) {
replica_id.0 += 1;
}
let new_collaborator = project_collaborator::ActiveModel {
project_id: ActiveValue::set(project_id),
connection_id: ActiveValue::set(connection.id as i32),
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
user_id: ActiveValue::set(participant.user_id),
replica_id: ActiveValue::set(replica_id),
is_host: ActiveValue::set(false),
..Default::default()
}
.insert(&*tx)
.await?;
collaborators.push(new_collaborator);
let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
let mut worktrees = db_worktrees
.into_iter() .into_iter()
.map(|db_worktree| { .map(|collaborator| collaborator.connection())
( .collect();
db_worktree.id as u64, Ok(LeftProject {
Worktree { id: project.id,
id: db_worktree.id as u64, connection_ids,
abs_path: db_worktree.abs_path, host_user_id: None,
root_name: db_worktree.root_name, host_connection_id: None,
visible: db_worktree.visible, })
entries: Default::default(),
repository_entries: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
},
)
})
.collect::<BTreeMap<_, _>>();
// Populate worktree entries.
{
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
.add(worktree_entry::Column::ProjectId.eq(project_id))
.add(worktree_entry::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
let db_entry = db_entry?;
if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) {
worktree.entries.push(proto::Entry {
id: db_entry.id as u64,
is_dir: db_entry.is_dir,
path: db_entry.path,
inode: db_entry.inode as u64,
mtime: Some(proto::Timestamp {
seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32,
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
git_status: db_entry.git_status.map(|status| status as i32),
});
}
}
}
// Populate repository entries.
{
let mut db_repository_entries = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project_id))
.add(worktree_repository::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_repository_entry) = db_repository_entries.next().await {
let db_repository_entry = db_repository_entry?;
if let Some(worktree) =
worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
{
worktree.repository_entries.insert(
db_repository_entry.work_directory_id as u64,
proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
},
);
}
}
}
// Populate worktree diagnostic summaries.
{
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
.filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id))
.stream(&*tx)
.await?;
while let Some(db_summary) = db_summaries.next().await {
let db_summary = db_summary?;
if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) {
worktree
.diagnostic_summaries
.push(proto::DiagnosticSummary {
path: db_summary.path,
language_server_id: db_summary.language_server_id as u64,
error_count: db_summary.error_count as u32,
warning_count: db_summary.warning_count as u32,
});
}
}
}
// Populate worktree settings files
{
let mut db_settings_files = worktree_settings_file::Entity::find()
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
.stream(&*tx)
.await?;
while let Some(db_settings_file) = db_settings_files.next().await {
let db_settings_file = db_settings_file?;
if let Some(worktree) =
worktrees.get_mut(&(db_settings_file.worktree_id as u64))
{
worktree.settings_files.push(WorktreeSettingsFile {
path: db_settings_file.path,
content: db_settings_file.content,
});
}
}
}
// Populate language servers.
let language_servers = project
.find_related(language_server::Entity)
.all(&*tx)
.await?;
let project = Project {
collaborators: collaborators
.into_iter()
.map(|collaborator| ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect(),
worktrees,
language_servers: language_servers
.into_iter()
.map(|language_server| proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
})
.collect(),
};
Ok((project, replica_id as ReplicaId))
}) })
.await .await
} }
@ -772,7 +859,7 @@ impl Database {
.exec(&*tx) .exec(&*tx)
.await?; .await?;
let room = self.get_room(project.room_id, &tx).await?; let room = self.get_room(room_id, &tx).await?;
let left_project = LeftProject { let left_project = LeftProject {
id: project_id, id: project_id,
host_user_id: project.host_user_id, host_user_id: project.host_user_id,
@ -996,7 +1083,9 @@ impl Database {
.one(&*tx) .one(&*tx)
.await? .await?
.ok_or_else(|| anyhow!("project {} not found", project_id))?; .ok_or_else(|| anyhow!("project {} not found", project_id))?;
Ok(project.room_id) Ok(project
.room_id
.ok_or_else(|| anyhow!("project not in room"))?)
}) })
.await .await
} }

View File

@ -491,7 +491,7 @@ impl Database {
.one(&*tx) .one(&*tx)
.await? .await?
.ok_or_else(|| anyhow!("project does not exist"))?; .ok_or_else(|| anyhow!("project does not exist"))?;
if project.host_user_id != user_id { if project.host_user_id != Some(user_id) {
return Err(anyhow!("no such project"))?; return Err(anyhow!("no such project"))?;
} }
@ -851,7 +851,7 @@ impl Database {
} }
if collaborator.is_host { if collaborator.is_host {
left_project.host_user_id = collaborator.user_id; left_project.host_user_id = Some(collaborator.user_id);
left_project.host_connection_id = Some(collaborator_connection_id); left_project.host_connection_id = Some(collaborator_connection_id);
} }
} }

View File

@ -1,4 +1,4 @@
use crate::db::{ProjectId, Result, RoomId, ServerId, UserId}; use crate::db::{HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId};
use anyhow::anyhow; use anyhow::anyhow;
use rpc::ConnectionId; use rpc::ConnectionId;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
@ -8,10 +8,11 @@ use sea_orm::entity::prelude::*;
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: ProjectId, pub id: ProjectId,
pub room_id: RoomId, pub room_id: Option<RoomId>,
pub host_user_id: UserId, pub host_user_id: Option<UserId>,
pub host_connection_id: Option<i32>, pub host_connection_id: Option<i32>,
pub host_connection_server_id: Option<ServerId>, pub host_connection_server_id: Option<ServerId>,
pub hosted_project_id: Option<HostedProjectId>,
} }
impl Model { impl Model {

View File

@ -4,8 +4,9 @@ use crate::{
auth::{self, Impersonator}, auth::{self, Impersonator},
db::{ db::{
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database, self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database,
InviteMemberResult, MembershipUpdated, MessageId, NotificationId, ProjectId, HostedProjectId, InviteMemberResult, MembershipUpdated, MessageId, NotificationId, Project,
RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, User, UserId, ProjectId, RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId, ServerId,
User, UserId,
}, },
executor::Executor, executor::Executor,
AppState, Error, Result, AppState, Error, Result,
@ -197,6 +198,7 @@ impl Server {
.add_request_handler(share_project) .add_request_handler(share_project)
.add_message_handler(unshare_project) .add_message_handler(unshare_project)
.add_request_handler(join_project) .add_request_handler(join_project)
.add_request_handler(join_hosted_project)
.add_message_handler(leave_project) .add_message_handler(leave_project)
.add_request_handler(update_project) .add_request_handler(update_project)
.add_request_handler(update_worktree) .add_request_handler(update_worktree)
@ -1584,22 +1586,46 @@ async fn join_project(
session: Session, session: Session,
) -> Result<()> { ) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id); let project_id = ProjectId::from_proto(request.project_id);
let guest_user_id = session.user_id;
tracing::info!(%project_id, "join project"); tracing::info!(%project_id, "join project");
let (project, replica_id) = &mut *session let (project, replica_id) = &mut *session
.db() .db()
.await .await
.join_project(project_id, session.connection_id) .join_project_in_room(project_id, session.connection_id)
.await?; .await?;
join_project_internal(response, session, project, replica_id)
}
trait JoinProjectInternalResponse {
fn send(self, result: proto::JoinProjectResponse) -> Result<()>;
}
impl JoinProjectInternalResponse for Response<proto::JoinProject> {
fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
Response::<proto::JoinProject>::send(self, result)
}
}
impl JoinProjectInternalResponse for Response<proto::JoinHostedProject> {
fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
Response::<proto::JoinHostedProject>::send(self, result)
}
}
fn join_project_internal(
response: impl JoinProjectInternalResponse,
session: Session,
project: &mut Project,
replica_id: &ReplicaId,
) -> Result<()> {
let collaborators = project let collaborators = project
.collaborators .collaborators
.iter() .iter()
.filter(|collaborator| collaborator.connection_id != session.connection_id) .filter(|collaborator| collaborator.connection_id != session.connection_id)
.map(|collaborator| collaborator.to_proto()) .map(|collaborator| collaborator.to_proto())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let project_id = project.id;
let guest_user_id = session.user_id;
let worktrees = project let worktrees = project
.worktrees .worktrees
@ -1631,10 +1657,12 @@ async fn join_project(
// First, we send the metadata associated with each worktree. // First, we send the metadata associated with each worktree.
response.send(proto::JoinProjectResponse { response.send(proto::JoinProjectResponse {
project_id: project.id.0 as u64,
worktrees: worktrees.clone(), worktrees: worktrees.clone(),
replica_id: replica_id.0 as u32, replica_id: replica_id.0 as u32,
collaborators: collaborators.clone(), collaborators: collaborators.clone(),
language_servers: project.language_servers.clone(), language_servers: project.language_servers.clone(),
role: project.role.into(), // todo
})?; })?;
for (worktree_id, worktree) in mem::take(&mut project.worktrees) { for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
@ -1707,15 +1735,17 @@ async fn join_project(
async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> { async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> {
let sender_id = session.connection_id; let sender_id = session.connection_id;
let project_id = ProjectId::from_proto(request.project_id); let project_id = ProjectId::from_proto(request.project_id);
let db = session.db().await;
if db.is_hosted_project(project_id).await? {
let project = db.leave_hosted_project(project_id, sender_id).await?;
project_left(&project, &session);
return Ok(());
}
let (room, project) = &*session let (room, project) = &*db.leave_project(project_id, sender_id).await?;
.db()
.await
.leave_project(project_id, sender_id)
.await?;
tracing::info!( tracing::info!(
%project_id, %project_id,
host_user_id = %project.host_user_id, host_user_id = ?project.host_user_id,
host_connection_id = ?project.host_connection_id, host_connection_id = ?project.host_connection_id,
"leave project" "leave project"
); );
@ -1726,6 +1756,24 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
Ok(()) Ok(())
} }
async fn join_hosted_project(
request: proto::JoinHostedProject,
response: Response<proto::JoinHostedProject>,
session: Session,
) -> Result<()> {
let (mut project, replica_id) = session
.db()
.await
.join_hosted_project(
HostedProjectId(request.id as i32),
session.user_id,
session.connection_id,
)
.await?;
join_project_internal(response, session, &mut project, &replica_id)
}
/// Updates other participants with changes to the project /// Updates other participants with changes to the project
async fn update_project( async fn update_project(
request: proto::UpdateProject, request: proto::UpdateProject,
@ -3624,7 +3672,7 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
fn project_left(project: &db::LeftProject, session: &Session) { fn project_left(project: &db::LeftProject, session: &Session) {
for connection_id in &project.connection_ids { for connection_id in &project.connection_ids {
if project.host_user_id == session.user_id { if project.host_user_id == Some(session.user_id) {
session session
.peer .peer
.send( .send(

View File

@ -1534,7 +1534,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
executor.run_until_parked(); executor.run_until_parked();
assert_eq!(visible_push_notifications(cx_a).len(), 1); assert_eq!(visible_push_notifications(cx_a).len(), 1);
cx_a.update(|cx| { cx_a.update(|cx| {
workspace::join_remote_project( workspace::join_in_room_project(
project_b_id, project_b_id,
client_b.user_id().unwrap(), client_b.user_id().unwrap(),
client_a.app_state.clone(), client_a.app_state.clone(),

View File

@ -22,7 +22,6 @@ use project::{
search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath, search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath,
}; };
use rand::prelude::*; use rand::prelude::*;
use rpc::proto::ChannelRole;
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use std::{ use std::{
@ -3742,7 +3741,6 @@ async fn test_leaving_project(
client_b.user_store().clone(), client_b.user_store().clone(),
client_b.language_registry().clone(), client_b.language_registry().clone(),
FakeFs::new(cx.background_executor().clone()), FakeFs::new(cx.background_executor().clone()),
ChannelRole::Member,
cx, cx,
) )
}) })

View File

@ -7,8 +7,8 @@ use crate::{
CollaborationPanelSettings, CollaborationPanelSettings,
}; };
use call::ActiveCall; use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelStore, HostedProjectId}; use channel::{Channel, ChannelEvent, ChannelStore};
use client::{ChannelId, Client, Contact, User, UserStore}; use client::{ChannelId, Client, Contact, HostedProjectId, User, UserStore};
use contact_finder::ContactFinder; use contact_finder::ContactFinder;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorElement, EditorStyle}; use editor::{Editor, EditorElement, EditorStyle};
@ -911,7 +911,7 @@ impl CollabPanel {
this.workspace this.workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
let app_state = workspace.app_state().clone(); let app_state = workspace.app_state().clone();
workspace::join_remote_project(project_id, host_user_id, app_state, cx) workspace::join_in_room_project(project_id, host_user_id, app_state, cx)
.detach_and_prompt_err("Failed to join project", cx, |_, _| None); .detach_and_prompt_err("Failed to join project", cx, |_, _| None);
}) })
.ok(); .ok();
@ -1047,8 +1047,15 @@ impl CollabPanel {
.indent_level(2) .indent_level(2)
.indent_step_size(px(20.)) .indent_step_size(px(20.))
.selected(is_selected) .selected(is_selected)
.on_click(cx.listener(move |_this, _, _cx| { .on_click(cx.listener(move |this, _, cx| {
// todo() if let Some(workspace) = this.workspace.upgrade() {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_hosted_project(id, app_state, cx).detach_and_prompt_err(
"Failed to open project",
cx,
|_, _| None,
)
}
})) }))
.start_slot( .start_slot(
h_flex() h_flex()
@ -1461,7 +1468,7 @@ impl CollabPanel {
} => { } => {
if let Some(workspace) = self.workspace.upgrade() { if let Some(workspace) = self.workspace.upgrade() {
let app_state = workspace.read(cx).app_state().clone(); let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project( workspace::join_in_room_project(
*project_id, *project_id,
*host_user_id, *host_user_id,
app_state, app_state,

View File

@ -82,7 +82,7 @@ impl IncomingCallNotificationState {
if let Some(project_id) = initial_project_id { if let Some(project_id) = initial_project_id {
cx.update(|cx| { cx.update(|cx| {
if let Some(app_state) = app_state.upgrade() { if let Some(app_state) = app_state.upgrade() {
workspace::join_remote_project( workspace::join_in_room_project(
project_id, project_id,
caller_user_id, caller_user_id,
app_state, app_state,

View File

@ -98,7 +98,7 @@ impl ProjectSharedNotification {
fn join(&mut self, cx: &mut ViewContext<Self>) { fn join(&mut self, cx: &mut ViewContext<Self>) {
if let Some(app_state) = self.app_state.upgrade() { if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx) workspace::join_in_room_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
} }

View File

@ -11,7 +11,7 @@ mod project_tests;
use anyhow::{anyhow, bail, Context as _, Result}; use anyhow::{anyhow, bail, Context as _, Result};
use async_trait::async_trait; use async_trait::async_trait;
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; use client::{proto, Client, Collaborator, HostedProjectId, TypedEnvelope, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
use copilot::Copilot; use copilot::Copilot;
@ -167,6 +167,7 @@ pub struct Project {
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>, prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
prettier_instances: HashMap<PathBuf, PrettierInstance>, prettier_instances: HashMap<PathBuf, PrettierInstance>,
tasks: Model<Inventory>, tasks: Model<Inventory>,
hosted_project_id: Option<HostedProjectId>,
} }
pub enum LanguageServerToQuery { pub enum LanguageServerToQuery {
@ -605,6 +606,7 @@ impl Project {
prettiers_per_worktree: HashMap::default(), prettiers_per_worktree: HashMap::default(),
prettier_instances: HashMap::default(), prettier_instances: HashMap::default(),
tasks, tasks,
hosted_project_id: None,
} }
}) })
} }
@ -615,17 +617,30 @@ impl Project {
user_store: Model<UserStore>, user_store: Model<UserStore>,
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
role: proto::ChannelRole, cx: AsyncAppContext,
mut cx: AsyncAppContext,
) -> Result<Model<Self>> { ) -> Result<Model<Self>> {
client.authenticate_and_connect(true, &cx).await?; client.authenticate_and_connect(true, &cx).await?;
let subscription = client.subscribe_to_entity(remote_id)?;
let response = client let response = client
.request_envelope(proto::JoinProject { .request_envelope(proto::JoinProject {
project_id: remote_id, project_id: remote_id,
}) })
.await?; .await?;
Self::from_join_project_response(response, None, client, user_store, languages, fs, cx)
.await
}
async fn from_join_project_response(
response: TypedEnvelope<proto::JoinProjectResponse>,
hosted_project_id: Option<HostedProjectId>,
client: Arc<Client>,
user_store: Model<UserStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
mut cx: AsyncAppContext,
) -> Result<Model<Self>> {
let remote_id = response.payload.project_id;
let role = response.payload.role();
let subscription = client.subscribe_to_entity(remote_id)?;
let this = cx.new_model(|cx| { let this = cx.new_model(|cx| {
let replica_id = response.payload.replica_id as ReplicaId; let replica_id = response.payload.replica_id as ReplicaId;
let tasks = Inventory::new(cx); let tasks = Inventory::new(cx);
@ -714,6 +729,7 @@ impl Project {
prettiers_per_worktree: HashMap::default(), prettiers_per_worktree: HashMap::default(),
prettier_instances: HashMap::default(), prettier_instances: HashMap::default(),
tasks, tasks,
hosted_project_id,
}; };
this.set_role(role, cx); this.set_role(role, cx);
for worktree in worktrees { for worktree in worktrees {
@ -742,6 +758,31 @@ impl Project {
Ok(this) Ok(this)
} }
pub async fn hosted(
hosted_project_id: HostedProjectId,
user_store: Model<UserStore>,
client: Arc<Client>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: AsyncAppContext,
) -> Result<Model<Self>> {
let response = client
.request_envelope(proto::JoinHostedProject {
id: hosted_project_id.0,
})
.await?;
Self::from_join_project_response(
response,
Some(hosted_project_id),
client,
user_store,
languages,
fs,
cx,
)
.await
}
fn release(&mut self, cx: &mut AppContext) { fn release(&mut self, cx: &mut AppContext) {
match &self.client_state { match &self.client_state {
ProjectClientState::Local => {} ProjectClientState::Local => {}
@ -987,6 +1028,10 @@ impl Project {
} }
} }
pub fn hosted_project_id(&self) -> Option<HostedProjectId> {
self.hosted_project_id
}
pub fn replica_id(&self) -> ReplicaId { pub fn replica_id(&self) -> ReplicaId {
match self.client_state { match self.client_state {
ProjectClientState::Remote { replica_id, .. } => replica_id, ProjectClientState::Remote { replica_id, .. } => replica_id,

View File

@ -196,6 +196,8 @@ message Envelope {
GetImplementation get_implementation = 162; GetImplementation get_implementation = 162;
GetImplementationResponse get_implementation_response = 163; GetImplementationResponse get_implementation_response = 163;
JoinHostedProject join_hosted_project = 164;
} }
reserved 158 to 161; reserved 158 to 161;
@ -230,6 +232,7 @@ enum ErrorCode {
CircularNesting = 10; CircularNesting = 10;
WrongMoveTarget = 11; WrongMoveTarget = 11;
UnsharedItem = 12; UnsharedItem = 12;
NoSuchProject = 13;
reserved 6; reserved 6;
} }
@ -404,11 +407,17 @@ message JoinProject {
uint64 project_id = 1; uint64 project_id = 1;
} }
message JoinHostedProject {
uint64 id = 1;
}
message JoinProjectResponse { message JoinProjectResponse {
uint64 project_id = 5;
uint32 replica_id = 1; uint32 replica_id = 1;
repeated WorktreeMetadata worktrees = 2; repeated WorktreeMetadata worktrees = 2;
repeated Collaborator collaborators = 3; repeated Collaborator collaborators = 3;
repeated LanguageServer language_servers = 4; repeated LanguageServer language_servers = 4;
ChannelRole role = 6;
} }
message LeaveProject { message LeaveProject {

View File

@ -206,6 +206,7 @@ messages!(
(JoinChannelChat, Foreground), (JoinChannelChat, Foreground),
(JoinChannelChatResponse, Foreground), (JoinChannelChatResponse, Foreground),
(JoinProject, Foreground), (JoinProject, Foreground),
(JoinHostedProject, Foreground),
(JoinProjectResponse, Foreground), (JoinProjectResponse, Foreground),
(JoinRoom, Foreground), (JoinRoom, Foreground),
(JoinRoomResponse, Foreground), (JoinRoomResponse, Foreground),
@ -329,6 +330,7 @@ request_messages!(
(JoinChannel, JoinRoomResponse), (JoinChannel, JoinRoomResponse),
(JoinChannelBuffer, JoinChannelBufferResponse), (JoinChannelBuffer, JoinChannelBufferResponse),
(JoinChannelChat, JoinChannelChatResponse), (JoinChannelChat, JoinChannelChatResponse),
(JoinHostedProject, JoinProjectResponse),
(JoinProject, JoinProjectResponse), (JoinProject, JoinProjectResponse),
(JoinRoom, JoinRoomResponse), (JoinRoom, JoinRoomResponse),
(LeaveChannelBuffer, Ack), (LeaveChannelBuffer, Ack),

View File

@ -268,7 +268,7 @@ impl Member {
this.cursor_pointer().on_mouse_down( this.cursor_pointer().on_mouse_down(
MouseButton::Left, MouseButton::Left,
cx.listener(move |this, _, cx| { cx.listener(move |this, _, cx| {
crate::join_remote_project( crate::join_in_room_project(
leader_project_id, leader_project_id,
leader_user_id, leader_user_id,
this.app_state().clone(), this.app_state().clone(),

View File

@ -15,7 +15,7 @@ use anyhow::{anyhow, Context as _, Result};
use call::{call_settings::CallSettings, ActiveCall}; use call::{call_settings::CallSettings, ActiveCall};
use client::{ use client::{
proto::{self, ErrorCode, PeerId}, proto::{self, ErrorCode, PeerId},
ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore, ChannelId, Client, ErrorExt, HostedProjectId, Status, TypedEnvelope, UserStore,
}; };
use collections::{hash_map, HashMap, HashSet}; use collections::{hash_map, HashMap, HashSet};
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
@ -2635,7 +2635,7 @@ impl Workspace {
// if they are active in another project, follow there. // if they are active in another project, follow there.
if let Some(project_id) = other_project_id { if let Some(project_id) = other_project_id {
let app_state = self.app_state.clone(); let app_state = self.app_state.clone();
crate::join_remote_project(project_id, remote_participant.user.id, app_state, cx) crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
@ -4158,7 +4158,7 @@ async fn join_channel_internal(
if let Some(room) = open_room { if let Some(room) = open_room {
let task = room.update(cx, |room, cx| { let task = room.update(cx, |room, cx| {
if let Some((project, host)) = room.most_active_project(cx) { if let Some((project, host)) = room.most_active_project(cx) {
return Some(join_remote_project(project, host, app_state.clone(), cx)); return Some(join_in_room_project(project, host, app_state.clone(), cx));
} }
None None
@ -4229,7 +4229,7 @@ async fn join_channel_internal(
let task = room.update(cx, |room, cx| { let task = room.update(cx, |room, cx| {
if let Some((project, host)) = room.most_active_project(cx) { if let Some((project, host)) = room.most_active_project(cx) {
return Some(join_remote_project(project, host, app_state.clone(), cx)); return Some(join_in_room_project(project, host, app_state.clone(), cx));
} }
// if you are the first to join a channel, share your project // if you are the first to join a channel, share your project
@ -4464,7 +4464,56 @@ pub fn create_and_open_local_file(
}) })
} }
pub fn join_remote_project( pub fn join_hosted_project(
hosted_project_id: HostedProjectId,
app_state: Arc<AppState>,
cx: &mut AppContext,
) -> Task<Result<()>> {
cx.spawn(|mut cx| async move {
let existing_window = cx.update(|cx| {
cx.windows().into_iter().find_map(|window| {
let workspace = window.downcast::<Workspace>()?;
workspace
.read(cx)
.is_ok_and(|workspace| {
workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id)
})
.then(|| workspace)
})
})?;
let workspace = if let Some(existing_window) = existing_window {
existing_window
} else {
let project = Project::hosted(
hosted_project_id,
app_state.user_store.clone(),
app_state.client.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx.clone(),
)
.await?;
let window_bounds_override = window_bounds_env_override(&cx);
cx.update(|cx| {
let options = (app_state.build_window_options)(window_bounds_override, None, cx);
cx.open_window(options, |cx| {
cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
})
})?
};
workspace.update(&mut cx, |_, cx| {
cx.activate(true);
cx.activate_window();
})?;
Ok(())
})
}
pub fn join_in_room_project(
project_id: u64, project_id: u64,
follow_user_id: u64, follow_user_id: u64,
app_state: Arc<AppState>, app_state: Arc<AppState>,