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