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