1use util::ResultExt;
2
3use super::*;
4
5impl Database {
6 /// Returns the count of all projects, excluding ones marked as admin.
7 pub async fn project_count_excluding_admins(&self) -> Result<usize> {
8 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
9 enum QueryAs {
10 Count,
11 }
12
13 self.transaction(|tx| async move {
14 Ok(project::Entity::find()
15 .select_only()
16 .column_as(project::Column::Id.count(), QueryAs::Count)
17 .inner_join(user::Entity)
18 .filter(user::Column::Admin.eq(false))
19 .into_values::<_, QueryAs>()
20 .one(&*tx)
21 .await?
22 .unwrap_or(0i64) as usize)
23 })
24 .await
25 }
26
27 /// Shares a project with the given room.
28 pub async fn share_project(
29 &self,
30 room_id: RoomId,
31 connection: ConnectionId,
32 worktrees: &[proto::WorktreeMetadata],
33 ) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
34 self.room_transaction(room_id, |tx| async move {
35 let participant = room_participant::Entity::find()
36 .filter(
37 Condition::all()
38 .add(
39 room_participant::Column::AnsweringConnectionId
40 .eq(connection.id as i32),
41 )
42 .add(
43 room_participant::Column::AnsweringConnectionServerId
44 .eq(connection.owner_id as i32),
45 ),
46 )
47 .one(&*tx)
48 .await?
49 .ok_or_else(|| anyhow!("could not find participant"))?;
50 if participant.room_id != room_id {
51 return Err(anyhow!("shared project on unexpected room"))?;
52 }
53 if !participant
54 .role
55 .unwrap_or(ChannelRole::Member)
56 .can_edit_projects()
57 {
58 return Err(anyhow!("guests cannot share projects"))?;
59 }
60
61 let project = project::ActiveModel {
62 room_id: ActiveValue::set(Some(participant.room_id)),
63 host_user_id: ActiveValue::set(Some(participant.user_id)),
64 host_connection_id: ActiveValue::set(Some(connection.id as i32)),
65 host_connection_server_id: ActiveValue::set(Some(ServerId(
66 connection.owner_id as i32,
67 ))),
68 id: ActiveValue::NotSet,
69 hosted_project_id: ActiveValue::Set(None),
70 remote_project_id: ActiveValue::Set(None),
71 }
72 .insert(&*tx)
73 .await?;
74
75 if !worktrees.is_empty() {
76 worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
77 worktree::ActiveModel {
78 id: ActiveValue::set(worktree.id as i64),
79 project_id: ActiveValue::set(project.id),
80 abs_path: ActiveValue::set(worktree.abs_path.clone()),
81 root_name: ActiveValue::set(worktree.root_name.clone()),
82 visible: ActiveValue::set(worktree.visible),
83 scan_id: ActiveValue::set(0),
84 completed_scan_id: ActiveValue::set(0),
85 }
86 }))
87 .exec(&*tx)
88 .await?;
89 }
90
91 project_collaborator::ActiveModel {
92 project_id: ActiveValue::set(project.id),
93 connection_id: ActiveValue::set(connection.id as i32),
94 connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
95 user_id: ActiveValue::set(participant.user_id),
96 replica_id: ActiveValue::set(ReplicaId(0)),
97 is_host: ActiveValue::set(true),
98 ..Default::default()
99 }
100 .insert(&*tx)
101 .await?;
102
103 let room = self.get_room(room_id, &tx).await?;
104 Ok((project.id, room))
105 })
106 .await
107 }
108
109 /// Unshares the given project.
110 pub async fn unshare_project(
111 &self,
112 project_id: ProjectId,
113 connection: ConnectionId,
114 ) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
115 self.project_transaction(project_id, |tx| async move {
116 let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
117 let project = project::Entity::find_by_id(project_id)
118 .one(&*tx)
119 .await?
120 .ok_or_else(|| anyhow!("project not found"))?;
121 if project.host_connection()? == connection {
122 let room = if let Some(room_id) = project.room_id {
123 Some(self.get_room(room_id, &tx).await?)
124 } else {
125 None
126 };
127 project::Entity::delete(project.into_active_model())
128 .exec(&*tx)
129 .await?;
130 Ok((room, guest_connection_ids))
131 } else {
132 Err(anyhow!("cannot unshare a project hosted by another user"))?
133 }
134 })
135 .await
136 }
137
138 /// Updates the worktrees associated with the given project.
139 pub async fn update_project(
140 &self,
141 project_id: ProjectId,
142 connection: ConnectionId,
143 worktrees: &[proto::WorktreeMetadata],
144 ) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
145 self.project_transaction(project_id, |tx| async move {
146 let project = project::Entity::find_by_id(project_id)
147 .filter(
148 Condition::all()
149 .add(project::Column::HostConnectionId.eq(connection.id as i32))
150 .add(
151 project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
152 ),
153 )
154 .one(&*tx)
155 .await?
156 .ok_or_else(|| anyhow!("no such project"))?;
157
158 self.update_project_worktrees(project.id, worktrees, &tx)
159 .await?;
160
161 let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
162
163 let room = if let Some(room_id) = project.room_id {
164 Some(self.get_room(room_id, &tx).await?)
165 } else {
166 None
167 };
168
169 Ok((room, guest_connection_ids))
170 })
171 .await
172 }
173
174 pub(in crate::db) async fn update_project_worktrees(
175 &self,
176 project_id: ProjectId,
177 worktrees: &[proto::WorktreeMetadata],
178 tx: &DatabaseTransaction,
179 ) -> Result<()> {
180 if !worktrees.is_empty() {
181 worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel {
182 id: ActiveValue::set(worktree.id as i64),
183 project_id: ActiveValue::set(project_id),
184 abs_path: ActiveValue::set(worktree.abs_path.clone()),
185 root_name: ActiveValue::set(worktree.root_name.clone()),
186 visible: ActiveValue::set(worktree.visible),
187 scan_id: ActiveValue::set(0),
188 completed_scan_id: ActiveValue::set(0),
189 }))
190 .on_conflict(
191 OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id])
192 .update_column(worktree::Column::RootName)
193 .to_owned(),
194 )
195 .exec(tx)
196 .await?;
197 }
198
199 worktree::Entity::delete_many()
200 .filter(worktree::Column::ProjectId.eq(project_id).and(
201 worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)),
202 ))
203 .exec(tx)
204 .await?;
205
206 Ok(())
207 }
208
209 pub async fn update_worktree(
210 &self,
211 update: &proto::UpdateWorktree,
212 connection: ConnectionId,
213 ) -> Result<TransactionGuard<Vec<ConnectionId>>> {
214 let project_id = ProjectId::from_proto(update.project_id);
215 let worktree_id = update.worktree_id as i64;
216 self.project_transaction(project_id, |tx| async move {
217 // Ensure the update comes from the host.
218 let _project = project::Entity::find_by_id(project_id)
219 .filter(
220 Condition::all()
221 .add(project::Column::HostConnectionId.eq(connection.id as i32))
222 .add(
223 project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
224 ),
225 )
226 .one(&*tx)
227 .await?
228 .ok_or_else(|| anyhow!("no such project"))?;
229
230 // Update metadata.
231 worktree::Entity::update(worktree::ActiveModel {
232 id: ActiveValue::set(worktree_id),
233 project_id: ActiveValue::set(project_id),
234 root_name: ActiveValue::set(update.root_name.clone()),
235 scan_id: ActiveValue::set(update.scan_id as i64),
236 completed_scan_id: if update.is_last_update {
237 ActiveValue::set(update.scan_id as i64)
238 } else {
239 ActiveValue::default()
240 },
241 abs_path: ActiveValue::set(update.abs_path.clone()),
242 ..Default::default()
243 })
244 .exec(&*tx)
245 .await?;
246
247 if !update.updated_entries.is_empty() {
248 worktree_entry::Entity::insert_many(update.updated_entries.iter().map(|entry| {
249 let mtime = entry.mtime.clone().unwrap_or_default();
250 worktree_entry::ActiveModel {
251 project_id: ActiveValue::set(project_id),
252 worktree_id: ActiveValue::set(worktree_id),
253 id: ActiveValue::set(entry.id as i64),
254 is_dir: ActiveValue::set(entry.is_dir),
255 path: ActiveValue::set(entry.path.clone()),
256 inode: ActiveValue::set(entry.inode as i64),
257 mtime_seconds: ActiveValue::set(mtime.seconds as i64),
258 mtime_nanos: ActiveValue::set(mtime.nanos as i32),
259 is_symlink: ActiveValue::set(entry.is_symlink),
260 is_ignored: ActiveValue::set(entry.is_ignored),
261 is_external: ActiveValue::set(entry.is_external),
262 git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
263 is_deleted: ActiveValue::set(false),
264 scan_id: ActiveValue::set(update.scan_id as i64),
265 }
266 }))
267 .on_conflict(
268 OnConflict::columns([
269 worktree_entry::Column::ProjectId,
270 worktree_entry::Column::WorktreeId,
271 worktree_entry::Column::Id,
272 ])
273 .update_columns([
274 worktree_entry::Column::IsDir,
275 worktree_entry::Column::Path,
276 worktree_entry::Column::Inode,
277 worktree_entry::Column::MtimeSeconds,
278 worktree_entry::Column::MtimeNanos,
279 worktree_entry::Column::IsSymlink,
280 worktree_entry::Column::IsIgnored,
281 worktree_entry::Column::GitStatus,
282 worktree_entry::Column::ScanId,
283 ])
284 .to_owned(),
285 )
286 .exec(&*tx)
287 .await?;
288 }
289
290 if !update.removed_entries.is_empty() {
291 worktree_entry::Entity::update_many()
292 .filter(
293 worktree_entry::Column::ProjectId
294 .eq(project_id)
295 .and(worktree_entry::Column::WorktreeId.eq(worktree_id))
296 .and(
297 worktree_entry::Column::Id
298 .is_in(update.removed_entries.iter().map(|id| *id as i64)),
299 ),
300 )
301 .set(worktree_entry::ActiveModel {
302 is_deleted: ActiveValue::Set(true),
303 scan_id: ActiveValue::Set(update.scan_id as i64),
304 ..Default::default()
305 })
306 .exec(&*tx)
307 .await?;
308 }
309
310 if !update.updated_repositories.is_empty() {
311 worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
312 |repository| worktree_repository::ActiveModel {
313 project_id: ActiveValue::set(project_id),
314 worktree_id: ActiveValue::set(worktree_id),
315 work_directory_id: ActiveValue::set(repository.work_directory_id as i64),
316 scan_id: ActiveValue::set(update.scan_id as i64),
317 branch: ActiveValue::set(repository.branch.clone()),
318 is_deleted: ActiveValue::set(false),
319 },
320 ))
321 .on_conflict(
322 OnConflict::columns([
323 worktree_repository::Column::ProjectId,
324 worktree_repository::Column::WorktreeId,
325 worktree_repository::Column::WorkDirectoryId,
326 ])
327 .update_columns([
328 worktree_repository::Column::ScanId,
329 worktree_repository::Column::Branch,
330 ])
331 .to_owned(),
332 )
333 .exec(&*tx)
334 .await?;
335 }
336
337 if !update.removed_repositories.is_empty() {
338 worktree_repository::Entity::update_many()
339 .filter(
340 worktree_repository::Column::ProjectId
341 .eq(project_id)
342 .and(worktree_repository::Column::WorktreeId.eq(worktree_id))
343 .and(
344 worktree_repository::Column::WorkDirectoryId
345 .is_in(update.removed_repositories.iter().map(|id| *id as i64)),
346 ),
347 )
348 .set(worktree_repository::ActiveModel {
349 is_deleted: ActiveValue::Set(true),
350 scan_id: ActiveValue::Set(update.scan_id as i64),
351 ..Default::default()
352 })
353 .exec(&*tx)
354 .await?;
355 }
356
357 let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
358 Ok(connection_ids)
359 })
360 .await
361 }
362
363 /// Updates the diagnostic summary for the given connection.
364 pub async fn update_diagnostic_summary(
365 &self,
366 update: &proto::UpdateDiagnosticSummary,
367 connection: ConnectionId,
368 ) -> Result<TransactionGuard<Vec<ConnectionId>>> {
369 let project_id = ProjectId::from_proto(update.project_id);
370 let worktree_id = update.worktree_id as i64;
371 self.project_transaction(project_id, |tx| async move {
372 let summary = update
373 .summary
374 .as_ref()
375 .ok_or_else(|| anyhow!("invalid summary"))?;
376
377 // Ensure the update comes from the host.
378 let project = project::Entity::find_by_id(project_id)
379 .one(&*tx)
380 .await?
381 .ok_or_else(|| anyhow!("no such project"))?;
382 if project.host_connection()? != connection {
383 return Err(anyhow!("can't update a project hosted by someone else"))?;
384 }
385
386 // Update summary.
387 worktree_diagnostic_summary::Entity::insert(worktree_diagnostic_summary::ActiveModel {
388 project_id: ActiveValue::set(project_id),
389 worktree_id: ActiveValue::set(worktree_id),
390 path: ActiveValue::set(summary.path.clone()),
391 language_server_id: ActiveValue::set(summary.language_server_id as i64),
392 error_count: ActiveValue::set(summary.error_count as i32),
393 warning_count: ActiveValue::set(summary.warning_count as i32),
394 })
395 .on_conflict(
396 OnConflict::columns([
397 worktree_diagnostic_summary::Column::ProjectId,
398 worktree_diagnostic_summary::Column::WorktreeId,
399 worktree_diagnostic_summary::Column::Path,
400 ])
401 .update_columns([
402 worktree_diagnostic_summary::Column::LanguageServerId,
403 worktree_diagnostic_summary::Column::ErrorCount,
404 worktree_diagnostic_summary::Column::WarningCount,
405 ])
406 .to_owned(),
407 )
408 .exec(&*tx)
409 .await?;
410
411 let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
412 Ok(connection_ids)
413 })
414 .await
415 }
416
417 /// Starts the language server for the given connection.
418 pub async fn start_language_server(
419 &self,
420 update: &proto::StartLanguageServer,
421 connection: ConnectionId,
422 ) -> Result<TransactionGuard<Vec<ConnectionId>>> {
423 let project_id = ProjectId::from_proto(update.project_id);
424 self.project_transaction(project_id, |tx| async move {
425 let server = update
426 .server
427 .as_ref()
428 .ok_or_else(|| anyhow!("invalid language server"))?;
429
430 // Ensure the update comes from the host.
431 let project = project::Entity::find_by_id(project_id)
432 .one(&*tx)
433 .await?
434 .ok_or_else(|| anyhow!("no such project"))?;
435 if project.host_connection()? != connection {
436 return Err(anyhow!("can't update a project hosted by someone else"))?;
437 }
438
439 // Add the newly-started language server.
440 language_server::Entity::insert(language_server::ActiveModel {
441 project_id: ActiveValue::set(project_id),
442 id: ActiveValue::set(server.id as i64),
443 name: ActiveValue::set(server.name.clone()),
444 })
445 .on_conflict(
446 OnConflict::columns([
447 language_server::Column::ProjectId,
448 language_server::Column::Id,
449 ])
450 .update_column(language_server::Column::Name)
451 .to_owned(),
452 )
453 .exec(&*tx)
454 .await?;
455
456 let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
457 Ok(connection_ids)
458 })
459 .await
460 }
461
462 /// Updates the worktree settings for the given connection.
463 pub async fn update_worktree_settings(
464 &self,
465 update: &proto::UpdateWorktreeSettings,
466 connection: ConnectionId,
467 ) -> Result<TransactionGuard<Vec<ConnectionId>>> {
468 let project_id = ProjectId::from_proto(update.project_id);
469 self.project_transaction(project_id, |tx| async move {
470 // Ensure the update comes from the host.
471 let project = project::Entity::find_by_id(project_id)
472 .one(&*tx)
473 .await?
474 .ok_or_else(|| anyhow!("no such project"))?;
475 if project.host_connection()? != connection {
476 return Err(anyhow!("can't update a project hosted by someone else"))?;
477 }
478
479 if let Some(content) = &update.content {
480 worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel {
481 project_id: ActiveValue::Set(project_id),
482 worktree_id: ActiveValue::Set(update.worktree_id as i64),
483 path: ActiveValue::Set(update.path.clone()),
484 content: ActiveValue::Set(content.clone()),
485 })
486 .on_conflict(
487 OnConflict::columns([
488 worktree_settings_file::Column::ProjectId,
489 worktree_settings_file::Column::WorktreeId,
490 worktree_settings_file::Column::Path,
491 ])
492 .update_column(worktree_settings_file::Column::Content)
493 .to_owned(),
494 )
495 .exec(&*tx)
496 .await?;
497 } else {
498 worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel {
499 project_id: ActiveValue::Set(project_id),
500 worktree_id: ActiveValue::Set(update.worktree_id as i64),
501 path: ActiveValue::Set(update.path.clone()),
502 ..Default::default()
503 })
504 .exec(&*tx)
505 .await?;
506 }
507
508 let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
509 Ok(connection_ids)
510 })
511 .await
512 }
513
514 /// Adds the given connection to the specified hosted project
515 pub async fn join_hosted_project(
516 &self,
517 id: ProjectId,
518 user_id: UserId,
519 connection: ConnectionId,
520 ) -> Result<(Project, ReplicaId)> {
521 self.transaction(|tx| async move {
522 let (project, hosted_project) = project::Entity::find_by_id(id)
523 .find_also_related(hosted_project::Entity)
524 .one(&*tx)
525 .await?
526 .ok_or_else(|| anyhow!("hosted project is no longer shared"))?;
527
528 let Some(hosted_project) = hosted_project else {
529 return Err(anyhow!("project is not hosted"))?;
530 };
531
532 let channel = channel::Entity::find_by_id(hosted_project.channel_id)
533 .one(&*tx)
534 .await?
535 .ok_or_else(|| anyhow!("no such channel"))?;
536
537 let role = self
538 .check_user_is_channel_participant(&channel, user_id, &tx)
539 .await?;
540
541 self.join_project_internal(project, user_id, connection, role, &tx)
542 .await
543 })
544 .await
545 }
546
547 pub async fn get_project(&self, id: ProjectId) -> Result<project::Model> {
548 self.transaction(|tx| async move {
549 Ok(project::Entity::find_by_id(id)
550 .one(&*tx)
551 .await?
552 .ok_or_else(|| anyhow!("no such project"))?)
553 })
554 .await
555 }
556
557 /// Adds the given connection to the specified project
558 /// in the current room.
559 pub async fn join_project(
560 &self,
561 project_id: ProjectId,
562 connection: ConnectionId,
563 user_id: UserId,
564 ) -> Result<TransactionGuard<(Project, ReplicaId)>> {
565 self.project_transaction(project_id, |tx| async move {
566 let (project, role) = self
567 .access_project(
568 project_id,
569 connection,
570 PrincipalId::UserId(user_id),
571 Capability::ReadOnly,
572 &tx,
573 )
574 .await?;
575 self.join_project_internal(project, user_id, connection, role, &tx)
576 .await
577 })
578 .await
579 }
580
581 async fn join_project_internal(
582 &self,
583 project: project::Model,
584 user_id: UserId,
585 connection: ConnectionId,
586 role: ChannelRole,
587 tx: &DatabaseTransaction,
588 ) -> Result<(Project, ReplicaId)> {
589 let mut collaborators = project
590 .find_related(project_collaborator::Entity)
591 .all(tx)
592 .await?;
593 let replica_ids = collaborators
594 .iter()
595 .map(|c| c.replica_id)
596 .collect::<HashSet<_>>();
597 let mut replica_id = ReplicaId(1);
598 while replica_ids.contains(&replica_id) {
599 replica_id.0 += 1;
600 }
601 let new_collaborator = project_collaborator::ActiveModel {
602 project_id: ActiveValue::set(project.id),
603 connection_id: ActiveValue::set(connection.id as i32),
604 connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
605 user_id: ActiveValue::set(user_id),
606 replica_id: ActiveValue::set(replica_id),
607 is_host: ActiveValue::set(false),
608 ..Default::default()
609 }
610 .insert(tx)
611 .await?;
612 collaborators.push(new_collaborator);
613
614 let db_worktrees = project.find_related(worktree::Entity).all(tx).await?;
615 let mut worktrees = db_worktrees
616 .into_iter()
617 .map(|db_worktree| {
618 (
619 db_worktree.id as u64,
620 Worktree {
621 id: db_worktree.id as u64,
622 abs_path: db_worktree.abs_path,
623 root_name: db_worktree.root_name,
624 visible: db_worktree.visible,
625 entries: Default::default(),
626 repository_entries: Default::default(),
627 diagnostic_summaries: Default::default(),
628 settings_files: Default::default(),
629 scan_id: db_worktree.scan_id as u64,
630 completed_scan_id: db_worktree.completed_scan_id as u64,
631 },
632 )
633 })
634 .collect::<BTreeMap<_, _>>();
635
636 // Populate worktree entries.
637 {
638 let mut db_entries = worktree_entry::Entity::find()
639 .filter(
640 Condition::all()
641 .add(worktree_entry::Column::ProjectId.eq(project.id))
642 .add(worktree_entry::Column::IsDeleted.eq(false)),
643 )
644 .stream(tx)
645 .await?;
646 while let Some(db_entry) = db_entries.next().await {
647 let db_entry = db_entry?;
648 if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) {
649 worktree.entries.push(proto::Entry {
650 id: db_entry.id as u64,
651 is_dir: db_entry.is_dir,
652 path: db_entry.path,
653 inode: db_entry.inode as u64,
654 mtime: Some(proto::Timestamp {
655 seconds: db_entry.mtime_seconds as u64,
656 nanos: db_entry.mtime_nanos as u32,
657 }),
658 is_symlink: db_entry.is_symlink,
659 is_ignored: db_entry.is_ignored,
660 is_external: db_entry.is_external,
661 git_status: db_entry.git_status.map(|status| status as i32),
662 });
663 }
664 }
665 }
666
667 // Populate repository entries.
668 {
669 let mut db_repository_entries = worktree_repository::Entity::find()
670 .filter(
671 Condition::all()
672 .add(worktree_repository::Column::ProjectId.eq(project.id))
673 .add(worktree_repository::Column::IsDeleted.eq(false)),
674 )
675 .stream(tx)
676 .await?;
677 while let Some(db_repository_entry) = db_repository_entries.next().await {
678 let db_repository_entry = db_repository_entry?;
679 if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
680 {
681 worktree.repository_entries.insert(
682 db_repository_entry.work_directory_id as u64,
683 proto::RepositoryEntry {
684 work_directory_id: db_repository_entry.work_directory_id as u64,
685 branch: db_repository_entry.branch,
686 },
687 );
688 }
689 }
690 }
691
692 // Populate worktree diagnostic summaries.
693 {
694 let mut db_summaries = worktree_diagnostic_summary::Entity::find()
695 .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project.id))
696 .stream(tx)
697 .await?;
698 while let Some(db_summary) = db_summaries.next().await {
699 let db_summary = db_summary?;
700 if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) {
701 worktree
702 .diagnostic_summaries
703 .push(proto::DiagnosticSummary {
704 path: db_summary.path,
705 language_server_id: db_summary.language_server_id as u64,
706 error_count: db_summary.error_count as u32,
707 warning_count: db_summary.warning_count as u32,
708 });
709 }
710 }
711 }
712
713 // Populate worktree settings files
714 {
715 let mut db_settings_files = worktree_settings_file::Entity::find()
716 .filter(worktree_settings_file::Column::ProjectId.eq(project.id))
717 .stream(tx)
718 .await?;
719 while let Some(db_settings_file) = db_settings_files.next().await {
720 let db_settings_file = db_settings_file?;
721 if let Some(worktree) = worktrees.get_mut(&(db_settings_file.worktree_id as u64)) {
722 worktree.settings_files.push(WorktreeSettingsFile {
723 path: db_settings_file.path,
724 content: db_settings_file.content,
725 });
726 }
727 }
728 }
729
730 // Populate language servers.
731 let language_servers = project
732 .find_related(language_server::Entity)
733 .all(tx)
734 .await?;
735
736 let project = Project {
737 id: project.id,
738 role,
739 collaborators: collaborators
740 .into_iter()
741 .map(|collaborator| ProjectCollaborator {
742 connection_id: collaborator.connection(),
743 user_id: collaborator.user_id,
744 replica_id: collaborator.replica_id,
745 is_host: collaborator.is_host,
746 })
747 .collect(),
748 worktrees,
749 language_servers: language_servers
750 .into_iter()
751 .map(|language_server| proto::LanguageServer {
752 id: language_server.id as u64,
753 name: language_server.name,
754 })
755 .collect(),
756 };
757 Ok((project, replica_id as ReplicaId))
758 }
759
760 pub async fn leave_hosted_project(
761 &self,
762 project_id: ProjectId,
763 connection: ConnectionId,
764 ) -> Result<LeftProject> {
765 self.transaction(|tx| async move {
766 let result = project_collaborator::Entity::delete_many()
767 .filter(
768 Condition::all()
769 .add(project_collaborator::Column::ProjectId.eq(project_id))
770 .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
771 .add(
772 project_collaborator::Column::ConnectionServerId
773 .eq(connection.owner_id as i32),
774 ),
775 )
776 .exec(&*tx)
777 .await?;
778 if result.rows_affected == 0 {
779 return Err(anyhow!("not in the project"))?;
780 }
781
782 let project = project::Entity::find_by_id(project_id)
783 .one(&*tx)
784 .await?
785 .ok_or_else(|| anyhow!("no such project"))?;
786 let collaborators = project
787 .find_related(project_collaborator::Entity)
788 .all(&*tx)
789 .await?;
790 let connection_ids = collaborators
791 .into_iter()
792 .map(|collaborator| collaborator.connection())
793 .collect();
794 Ok(LeftProject {
795 id: project.id,
796 connection_ids,
797 host_user_id: None,
798 host_connection_id: None,
799 })
800 })
801 .await
802 }
803
804 /// Removes the given connection from the specified project.
805 pub async fn leave_project(
806 &self,
807 project_id: ProjectId,
808 connection: ConnectionId,
809 ) -> Result<TransactionGuard<(Option<proto::Room>, LeftProject)>> {
810 self.project_transaction(project_id, |tx| async move {
811 let result = project_collaborator::Entity::delete_many()
812 .filter(
813 Condition::all()
814 .add(project_collaborator::Column::ProjectId.eq(project_id))
815 .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
816 .add(
817 project_collaborator::Column::ConnectionServerId
818 .eq(connection.owner_id as i32),
819 ),
820 )
821 .exec(&*tx)
822 .await?;
823 if result.rows_affected == 0 {
824 Err(anyhow!("not a collaborator on this project"))?;
825 }
826
827 let project = project::Entity::find_by_id(project_id)
828 .one(&*tx)
829 .await?
830 .ok_or_else(|| anyhow!("no such project"))?;
831 let collaborators = project
832 .find_related(project_collaborator::Entity)
833 .all(&*tx)
834 .await?;
835 let connection_ids = collaborators
836 .into_iter()
837 .map(|collaborator| collaborator.connection())
838 .collect();
839
840 follower::Entity::delete_many()
841 .filter(
842 Condition::any()
843 .add(
844 Condition::all()
845 .add(follower::Column::ProjectId.eq(Some(project_id)))
846 .add(
847 follower::Column::LeaderConnectionServerId
848 .eq(connection.owner_id),
849 )
850 .add(follower::Column::LeaderConnectionId.eq(connection.id)),
851 )
852 .add(
853 Condition::all()
854 .add(follower::Column::ProjectId.eq(Some(project_id)))
855 .add(
856 follower::Column::FollowerConnectionServerId
857 .eq(connection.owner_id),
858 )
859 .add(follower::Column::FollowerConnectionId.eq(connection.id)),
860 ),
861 )
862 .exec(&*tx)
863 .await?;
864
865 let room = if let Some(room_id) = project.room_id {
866 Some(self.get_room(room_id, &tx).await?)
867 } else {
868 None
869 };
870
871 let left_project = LeftProject {
872 id: project_id,
873 host_user_id: project.host_user_id,
874 host_connection_id: Some(project.host_connection()?),
875 connection_ids,
876 };
877 Ok((room, left_project))
878 })
879 .await
880 }
881
882 pub async fn check_user_is_project_host(
883 &self,
884 project_id: ProjectId,
885 connection_id: ConnectionId,
886 ) -> Result<()> {
887 self.project_transaction(project_id, |tx| async move {
888 project::Entity::find()
889 .filter(
890 Condition::all()
891 .add(project::Column::Id.eq(project_id))
892 .add(project::Column::HostConnectionId.eq(Some(connection_id.id as i32)))
893 .add(
894 project::Column::HostConnectionServerId
895 .eq(Some(connection_id.owner_id as i32)),
896 ),
897 )
898 .one(&*tx)
899 .await?
900 .ok_or_else(|| anyhow!("failed to read project host"))?;
901
902 Ok(())
903 })
904 .await
905 .map(|guard| guard.into_inner())
906 }
907
908 /// Returns the current project if the given user is authorized to access it with the specified capability.
909 pub async fn access_project(
910 &self,
911 project_id: ProjectId,
912 connection_id: ConnectionId,
913 principal_id: PrincipalId,
914 capability: Capability,
915 tx: &DatabaseTransaction,
916 ) -> Result<(project::Model, ChannelRole)> {
917 let (project, remote_project) = project::Entity::find_by_id(project_id)
918 .find_also_related(remote_project::Entity)
919 .one(tx)
920 .await?
921 .ok_or_else(|| anyhow!("no such project"))?;
922
923 let user_id = match principal_id {
924 PrincipalId::DevServerId(_) => {
925 if project
926 .host_connection()
927 .is_ok_and(|connection| connection == connection_id)
928 {
929 return Ok((project, ChannelRole::Admin));
930 }
931 return Err(anyhow!("not the project host"))?;
932 }
933 PrincipalId::UserId(user_id) => user_id,
934 };
935
936 let role = if let Some(remote_project) = remote_project {
937 let channel = channel::Entity::find_by_id(remote_project.channel_id)
938 .one(tx)
939 .await?
940 .ok_or_else(|| anyhow!("no such channel"))?;
941
942 self.check_user_is_channel_participant(&channel, user_id, &tx)
943 .await?
944 } else if let Some(room_id) = project.room_id {
945 // what's the users role?
946 let current_participant = room_participant::Entity::find()
947 .filter(room_participant::Column::RoomId.eq(room_id))
948 .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
949 .one(tx)
950 .await?
951 .ok_or_else(|| anyhow!("no such room"))?;
952
953 current_participant.role.unwrap_or(ChannelRole::Guest)
954 } else {
955 return Err(anyhow!("not authorized to read projects"))?;
956 };
957
958 match capability {
959 Capability::ReadWrite => {
960 if !role.can_edit_projects() {
961 return Err(anyhow!("not authorized to edit projects"))?;
962 }
963 }
964 Capability::ReadOnly => {
965 if !role.can_read_projects() {
966 return Err(anyhow!("not authorized to read projects"))?;
967 }
968 }
969 }
970
971 Ok((project, role))
972 }
973
974 /// Returns the host connection for a read-only request to join a shared project.
975 pub async fn host_for_read_only_project_request(
976 &self,
977 project_id: ProjectId,
978 connection_id: ConnectionId,
979 user_id: UserId,
980 ) -> Result<ConnectionId> {
981 self.project_transaction(project_id, |tx| async move {
982 let (project, _) = self
983 .access_project(
984 project_id,
985 connection_id,
986 PrincipalId::UserId(user_id),
987 Capability::ReadOnly,
988 &tx,
989 )
990 .await?;
991 project.host_connection()
992 })
993 .await
994 .map(|guard| guard.into_inner())
995 }
996
997 /// Returns the host connection for a request to join a shared project.
998 pub async fn host_for_mutating_project_request(
999 &self,
1000 project_id: ProjectId,
1001 connection_id: ConnectionId,
1002 user_id: UserId,
1003 ) -> Result<ConnectionId> {
1004 self.project_transaction(project_id, |tx| async move {
1005 let (project, _) = self
1006 .access_project(
1007 project_id,
1008 connection_id,
1009 PrincipalId::UserId(user_id),
1010 Capability::ReadWrite,
1011 &tx,
1012 )
1013 .await?;
1014 project.host_connection()
1015 })
1016 .await
1017 .map(|guard| guard.into_inner())
1018 }
1019
1020 pub async fn connections_for_buffer_update(
1021 &self,
1022 project_id: ProjectId,
1023 principal_id: PrincipalId,
1024 connection_id: ConnectionId,
1025 capability: Capability,
1026 ) -> Result<TransactionGuard<(ConnectionId, Vec<ConnectionId>)>> {
1027 self.project_transaction(project_id, |tx| async move {
1028 // Authorize
1029 let (project, _) = self
1030 .access_project(project_id, connection_id, principal_id, capability, &tx)
1031 .await?;
1032
1033 let host_connection_id = project.host_connection()?;
1034
1035 let collaborators = project_collaborator::Entity::find()
1036 .filter(project_collaborator::Column::ProjectId.eq(project_id))
1037 .all(&*tx)
1038 .await?;
1039
1040 let guest_connection_ids = collaborators
1041 .into_iter()
1042 .filter_map(|collaborator| {
1043 if collaborator.is_host {
1044 None
1045 } else {
1046 Some(collaborator.connection())
1047 }
1048 })
1049 .collect();
1050
1051 Ok((host_connection_id, guest_connection_ids))
1052 })
1053 .await
1054 }
1055
1056 /// Returns the connection IDs in the given project.
1057 ///
1058 /// The provided `connection_id` must also be a collaborator in the project,
1059 /// otherwise an error will be returned.
1060 pub async fn project_connection_ids(
1061 &self,
1062 project_id: ProjectId,
1063 connection_id: ConnectionId,
1064 exclude_dev_server: bool,
1065 ) -> Result<TransactionGuard<HashSet<ConnectionId>>> {
1066 self.project_transaction(project_id, |tx| async move {
1067 let project = project::Entity::find_by_id(project_id)
1068 .one(&*tx)
1069 .await?
1070 .ok_or_else(|| anyhow!("no such project"))?;
1071
1072 let mut collaborators = project_collaborator::Entity::find()
1073 .filter(project_collaborator::Column::ProjectId.eq(project_id))
1074 .stream(&*tx)
1075 .await?;
1076
1077 let mut connection_ids = HashSet::default();
1078 if let Some(host_connection) = project.host_connection().log_err() {
1079 if !exclude_dev_server {
1080 connection_ids.insert(host_connection);
1081 }
1082 }
1083
1084 while let Some(collaborator) = collaborators.next().await {
1085 let collaborator = collaborator?;
1086 connection_ids.insert(collaborator.connection());
1087 }
1088
1089 if connection_ids.contains(&connection_id)
1090 || Some(connection_id) == project.host_connection().ok()
1091 {
1092 Ok(connection_ids)
1093 } else {
1094 Err(anyhow!(
1095 "can only send project updates to a project you're in"
1096 ))?
1097 }
1098 })
1099 .await
1100 }
1101
1102 async fn project_guest_connection_ids(
1103 &self,
1104 project_id: ProjectId,
1105 tx: &DatabaseTransaction,
1106 ) -> Result<Vec<ConnectionId>> {
1107 let mut collaborators = project_collaborator::Entity::find()
1108 .filter(
1109 project_collaborator::Column::ProjectId
1110 .eq(project_id)
1111 .and(project_collaborator::Column::IsHost.eq(false)),
1112 )
1113 .stream(tx)
1114 .await?;
1115
1116 let mut guest_connection_ids = Vec::new();
1117 while let Some(collaborator) = collaborators.next().await {
1118 let collaborator = collaborator?;
1119 guest_connection_ids.push(collaborator.connection());
1120 }
1121 Ok(guest_connection_ids)
1122 }
1123
1124 /// Returns the [`RoomId`] for the given project.
1125 pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result<Option<RoomId>> {
1126 self.transaction(|tx| async move {
1127 Ok(project::Entity::find_by_id(project_id)
1128 .one(&*tx)
1129 .await?
1130 .and_then(|project| project.room_id))
1131 })
1132 .await
1133 }
1134
1135 pub async fn check_room_participants(
1136 &self,
1137 room_id: RoomId,
1138 leader_id: ConnectionId,
1139 follower_id: ConnectionId,
1140 ) -> Result<()> {
1141 self.transaction(|tx| async move {
1142 use room_participant::Column;
1143
1144 let count = room_participant::Entity::find()
1145 .filter(
1146 Condition::all().add(Column::RoomId.eq(room_id)).add(
1147 Condition::any()
1148 .add(Column::AnsweringConnectionId.eq(leader_id.id as i32).and(
1149 Column::AnsweringConnectionServerId.eq(leader_id.owner_id as i32),
1150 ))
1151 .add(Column::AnsweringConnectionId.eq(follower_id.id as i32).and(
1152 Column::AnsweringConnectionServerId.eq(follower_id.owner_id as i32),
1153 )),
1154 ),
1155 )
1156 .count(&*tx)
1157 .await?;
1158
1159 if count < 2 {
1160 Err(anyhow!("not room participants"))?;
1161 }
1162
1163 Ok(())
1164 })
1165 .await
1166 }
1167
1168 /// Adds the given follower connection as a follower of the given leader connection.
1169 pub async fn follow(
1170 &self,
1171 room_id: RoomId,
1172 project_id: ProjectId,
1173 leader_connection: ConnectionId,
1174 follower_connection: ConnectionId,
1175 ) -> Result<TransactionGuard<proto::Room>> {
1176 self.room_transaction(room_id, |tx| async move {
1177 follower::ActiveModel {
1178 room_id: ActiveValue::set(room_id),
1179 project_id: ActiveValue::set(project_id),
1180 leader_connection_server_id: ActiveValue::set(ServerId(
1181 leader_connection.owner_id as i32,
1182 )),
1183 leader_connection_id: ActiveValue::set(leader_connection.id as i32),
1184 follower_connection_server_id: ActiveValue::set(ServerId(
1185 follower_connection.owner_id as i32,
1186 )),
1187 follower_connection_id: ActiveValue::set(follower_connection.id as i32),
1188 ..Default::default()
1189 }
1190 .insert(&*tx)
1191 .await?;
1192
1193 let room = self.get_room(room_id, &tx).await?;
1194 Ok(room)
1195 })
1196 .await
1197 }
1198
1199 /// Removes the given follower connection as a follower of the given leader connection.
1200 pub async fn unfollow(
1201 &self,
1202 room_id: RoomId,
1203 project_id: ProjectId,
1204 leader_connection: ConnectionId,
1205 follower_connection: ConnectionId,
1206 ) -> Result<TransactionGuard<proto::Room>> {
1207 self.room_transaction(room_id, |tx| async move {
1208 follower::Entity::delete_many()
1209 .filter(
1210 Condition::all()
1211 .add(follower::Column::RoomId.eq(room_id))
1212 .add(follower::Column::ProjectId.eq(project_id))
1213 .add(
1214 follower::Column::LeaderConnectionServerId
1215 .eq(leader_connection.owner_id),
1216 )
1217 .add(follower::Column::LeaderConnectionId.eq(leader_connection.id))
1218 .add(
1219 follower::Column::FollowerConnectionServerId
1220 .eq(follower_connection.owner_id),
1221 )
1222 .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)),
1223 )
1224 .exec(&*tx)
1225 .await?;
1226
1227 let room = self.get_room(room_id, &tx).await?;
1228 Ok(room)
1229 })
1230 .await
1231 }
1232}