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        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}