projects.rs

   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}