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