1401 lines · 56.9 KB · Rust
RawDownload
use anyhow::Context as _;use collections::HashSet;use util::ResultExt; use super::*; impl Database {    /// Returns the count of all projects, excluding ones marked as admin.    pub async fn project_count_excluding_admins(&self) -> Result<usize> {        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]        enum QueryAs {            Count,        }         self.transaction(|tx| async move {            Ok(project::Entity::find()                .select_only()                .column_as(project::Column::Id.count(), QueryAs::Count)                .inner_join(user::Entity)                .filter(user::Column::Admin.eq(false))                .into_values::<_, QueryAs>()                .one(&*tx)                .await?                .unwrap_or(0i64) as usize)        })        .await    }     /// Shares a project with the given room.    pub async fn share_project(        &self,        room_id: RoomId,        connection: ConnectionId,        worktrees: &[proto::WorktreeMetadata],        is_ssh_project: bool,        windows_paths: bool,        features: &[String],    ) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {        self.room_transaction(room_id, |tx| async move {            let participant = room_participant::Entity::find()                .filter(                    Condition::all()                        .add(                            room_participant::Column::AnsweringConnectionId                                .eq(connection.id as i32),                        )                        .add(                            room_participant::Column::AnsweringConnectionServerId                                .eq(connection.owner_id as i32),                        ),                )                .one(&*tx)                .await?                .context("could not find participant")?;            if participant.room_id != room_id {                return Err(anyhow!("shared project on unexpected room"))?;            }            if !participant                .role                .unwrap_or(ChannelRole::Member)                .can_edit_projects()            {                return Err(anyhow!("guests cannot share projects"))?;            }             let project = project::ActiveModel {                room_id: ActiveValue::set(Some(participant.room_id)),                host_user_id: ActiveValue::set(Some(participant.user_id)),                host_connection_id: ActiveValue::set(Some(connection.id as i32)),                host_connection_server_id: ActiveValue::set(Some(ServerId(                    connection.owner_id as i32,                ))),                id: ActiveValue::NotSet,                windows_paths: ActiveValue::set(windows_paths),                features: ActiveValue::set(serde_json::to_string(features).unwrap()),            }            .insert(&*tx)            .await?;             if !worktrees.is_empty() {                worktree::Entity::insert_many(worktrees.iter().map(|worktree| {                    worktree::ActiveModel {                        id: ActiveValue::set(worktree.id as i64),                        project_id: ActiveValue::set(project.id),                        abs_path: ActiveValue::set(worktree.abs_path.clone()),                        root_name: ActiveValue::set(worktree.root_name.clone()),                        visible: ActiveValue::set(worktree.visible),                        scan_id: ActiveValue::set(0),                        completed_scan_id: ActiveValue::set(0),                        root_repo_common_dir: ActiveValue::set(None),                    }                }))                .exec(&*tx)                .await?;            }             let replica_id = if is_ssh_project {                clock::ReplicaId::REMOTE_SERVER            } else {                clock::ReplicaId::LOCAL            };             project_collaborator::ActiveModel {                project_id: ActiveValue::set(project.id),                connection_id: ActiveValue::set(connection.id as i32),                connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),                user_id: ActiveValue::set(participant.user_id),                replica_id: ActiveValue::set(ReplicaId(replica_id.as_u16() as i32)),                is_host: ActiveValue::set(true),                id: ActiveValue::NotSet,                committer_name: ActiveValue::Set(None),                committer_email: ActiveValue::Set(None),            }            .insert(&*tx)            .await?;             let room = self.get_room(room_id, &tx).await?;            Ok((project.id, room))        })        .await    }     pub async fn delete_project(&self, project_id: ProjectId) -> Result<()> {        self.transaction(|tx| async move {            project::Entity::delete_by_id(project_id).exec(&*tx).await?;            Ok(())        })        .await    }     /// Unshares the given project.    pub async fn unshare_project(        &self,        project_id: ProjectId,        connection: ConnectionId,    ) -> Result<TransactionGuard<(bool, Option<proto::Room>, Vec<ConnectionId>)>> {        self.project_transaction(project_id, |tx| async move {            let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;            let project = project::Entity::find_by_id(project_id)                .one(&*tx)                .await?                .context("project not found")?;            let room = if let Some(room_id) = project.room_id {                Some(self.get_room(room_id, &tx).await?)            } else {                None            };            if project.host_connection()? == connection {                return Ok((true, room, guest_connection_ids));            }            Err(anyhow!("cannot unshare a project hosted by another user"))?        })        .await    }     /// Updates the worktrees associated with the given project.    pub async fn update_project(        &self,        project_id: ProjectId,        connection: ConnectionId,        worktrees: &[proto::WorktreeMetadata],    ) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {        self.project_transaction(project_id, |tx| async move {            let project = project::Entity::find_by_id(project_id)                .filter(                    Condition::all()                        .add(project::Column::HostConnectionId.eq(connection.id as i32))                        .add(                            project::Column::HostConnectionServerId.eq(connection.owner_id as i32),                        ),                )                .one(&*tx)                .await?                .context("no such project")?;             self.update_project_worktrees(project.id, worktrees, &tx)                .await?;             let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;             let room = if let Some(room_id) = project.room_id {                Some(self.get_room(room_id, &tx).await?)            } else {                None            };             Ok((room, guest_connection_ids))        })        .await    }     pub(in crate::db) async fn update_project_worktrees(        &self,        project_id: ProjectId,        worktrees: &[proto::WorktreeMetadata],        tx: &DatabaseTransaction,    ) -> Result<()> {        if !worktrees.is_empty() {            worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel {                id: ActiveValue::set(worktree.id as i64),                project_id: ActiveValue::set(project_id),                abs_path: ActiveValue::set(worktree.abs_path.clone()),                root_name: ActiveValue::set(worktree.root_name.clone()),                visible: ActiveValue::set(worktree.visible),                scan_id: ActiveValue::set(0),                completed_scan_id: ActiveValue::set(0),                root_repo_common_dir: ActiveValue::set(None),            }))            .on_conflict(                OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id])                    .update_column(worktree::Column::RootName)                    .to_owned(),            )            .exec(tx)            .await?;        }         worktree::Entity::delete_many()            .filter(worktree::Column::ProjectId.eq(project_id).and(                worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)),            ))            .exec(tx)            .await?;         Ok(())    }     pub async fn update_worktree(        &self,        update: &proto::UpdateWorktree,        connection: ConnectionId,    ) -> Result<TransactionGuard<Vec<ConnectionId>>> {        if update.removed_entries.len() > proto::MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE            || update.updated_entries.len() > proto::MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE        {            return Err(anyhow!(                "invalid worktree update. removed entries: {}, updated entries: {}",                update.removed_entries.len(),                update.updated_entries.len()            ))?;        }         let project_id = ProjectId::from_proto(update.project_id);        let worktree_id = update.worktree_id as i64;        self.project_transaction(project_id, |tx| async move {            // Ensure the update comes from the host.            let _project = project::Entity::find_by_id(project_id)                .filter(                    Condition::all()                        .add(project::Column::HostConnectionId.eq(connection.id as i32))                        .add(                            project::Column::HostConnectionServerId.eq(connection.owner_id as i32),                        ),                )                .one(&*tx)                .await?                .with_context(|| format!("no such project: {project_id}"))?;             // Update metadata.            worktree::Entity::update(worktree::ActiveModel {                id: ActiveValue::set(worktree_id),                project_id: ActiveValue::set(project_id),                root_name: ActiveValue::set(update.root_name.clone()),                scan_id: ActiveValue::set(update.scan_id as i64),                completed_scan_id: if update.is_last_update {                    ActiveValue::set(update.scan_id as i64)                } else {                    ActiveValue::default()                },                abs_path: ActiveValue::set(update.abs_path.clone()),                root_repo_common_dir: ActiveValue::set(update.root_repo_common_dir.clone()),                ..Default::default()            })            .exec(&*tx)            .await?;             if !update.updated_entries.is_empty() {                worktree_entry::Entity::insert_many(update.updated_entries.iter().map(|entry| {                    let mtime = entry.mtime.clone().unwrap_or_default();                    worktree_entry::ActiveModel {                        project_id: ActiveValue::set(project_id),                        worktree_id: ActiveValue::set(worktree_id),                        id: ActiveValue::set(entry.id as i64),                        is_dir: ActiveValue::set(entry.is_dir),                        path: ActiveValue::set(entry.path.clone()),                        inode: ActiveValue::set(entry.inode as i64),                        mtime_seconds: ActiveValue::set(mtime.seconds as i64),                        mtime_nanos: ActiveValue::set(mtime.nanos as i32),                        canonical_path: ActiveValue::set(entry.canonical_path.clone()),                        is_ignored: ActiveValue::set(entry.is_ignored),                        git_status: ActiveValue::set(None),                        is_external: ActiveValue::set(entry.is_external),                        is_deleted: ActiveValue::set(false),                        is_hidden: ActiveValue::set(entry.is_hidden),                        scan_id: ActiveValue::set(update.scan_id as i64),                        is_fifo: ActiveValue::set(entry.is_fifo),                    }                }))                .on_conflict(                    OnConflict::columns([                        worktree_entry::Column::ProjectId,                        worktree_entry::Column::WorktreeId,                        worktree_entry::Column::Id,                    ])                    .update_columns([                        worktree_entry::Column::IsDir,                        worktree_entry::Column::Path,                        worktree_entry::Column::Inode,                        worktree_entry::Column::MtimeSeconds,                        worktree_entry::Column::MtimeNanos,                        worktree_entry::Column::CanonicalPath,                        worktree_entry::Column::IsIgnored,                        worktree_entry::Column::IsHidden,                        worktree_entry::Column::ScanId,                    ])                    .to_owned(),                )                .exec(&*tx)                .await?;            }             if !update.removed_entries.is_empty() {                worktree_entry::Entity::update_many()                    .filter(                        worktree_entry::Column::ProjectId                            .eq(project_id)                            .and(worktree_entry::Column::WorktreeId.eq(worktree_id))                            .and(                                worktree_entry::Column::Id                                    .is_in(update.removed_entries.iter().map(|id| *id as i64)),                            ),                    )                    .set(worktree_entry::ActiveModel {                        is_deleted: ActiveValue::Set(true),                        scan_id: ActiveValue::Set(update.scan_id as i64),                        ..Default::default()                    })                    .exec(&*tx)                    .await?;            }             let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;            Ok(connection_ids)        })        .await    }     pub async fn update_repository(        &self,        update: &proto::UpdateRepository,        _connection: ConnectionId,    ) -> Result<TransactionGuard<Vec<ConnectionId>>> {        let project_id = ProjectId::from_proto(update.project_id);        let repository_id = update.id as i64;        self.project_transaction(project_id, |tx| async move {            project_repository::Entity::insert(project_repository::ActiveModel {                project_id: ActiveValue::set(project_id),                id: ActiveValue::set(repository_id),                legacy_worktree_id: ActiveValue::set(None),                abs_path: ActiveValue::set(update.abs_path.clone()),                entry_ids: ActiveValue::Set(serde_json::to_string(&update.entry_ids).unwrap()),                scan_id: ActiveValue::set(update.scan_id as i64),                is_deleted: ActiveValue::set(false),                branch_summary: ActiveValue::Set(                    update                        .branch_summary                        .as_ref()                        .map(|summary| serde_json::to_string(summary).unwrap()),                ),                head_commit_details: ActiveValue::Set(                    update                        .head_commit_details                        .as_ref()                        .map(|details| serde_json::to_string(details).unwrap()),                ),                current_merge_conflicts: ActiveValue::Set(Some(                    serde_json::to_string(&update.current_merge_conflicts).unwrap(),                )),                merge_message: ActiveValue::set(update.merge_message.clone()),                remote_upstream_url: ActiveValue::set(update.remote_upstream_url.clone()),                remote_origin_url: ActiveValue::set(update.remote_origin_url.clone()),                repository_dir_abs_path: ActiveValue::set(update.repository_dir_abs_path.clone()),                common_dir_abs_path: ActiveValue::set(update.common_dir_abs_path.clone()),                linked_worktrees: ActiveValue::Set(Some(                    serde_json::to_string(&update.linked_worktrees).unwrap(),                )),            })            .on_conflict(                OnConflict::columns([                    project_repository::Column::ProjectId,                    project_repository::Column::Id,                ])                .update_columns([                    project_repository::Column::ScanId,                    project_repository::Column::BranchSummary,                    project_repository::Column::EntryIds,                    project_repository::Column::AbsPath,                    project_repository::Column::CurrentMergeConflicts,                    project_repository::Column::HeadCommitDetails,                    project_repository::Column::MergeMessage,                    project_repository::Column::RepositoryDirAbsPath,                    project_repository::Column::CommonDirAbsPath,                    project_repository::Column::LinkedWorktrees,                ])                .to_owned(),            )            .exec(&*tx)            .await?;             let has_any_statuses = !update.updated_statuses.is_empty();             if has_any_statuses {                project_repository_statuses::Entity::insert_many(                    update.updated_statuses.iter().map(|status_entry| {                        let (repo_path, status_kind, first_status, second_status) =                            proto_status_to_db(status_entry.clone());                        project_repository_statuses::ActiveModel {                            project_id: ActiveValue::set(project_id),                            repository_id: ActiveValue::set(repository_id),                            scan_id: ActiveValue::set(update.scan_id as i64),                            is_deleted: ActiveValue::set(false),                            repo_path: ActiveValue::set(repo_path),                            status: ActiveValue::set(0),                            status_kind: ActiveValue::set(status_kind),                            first_status: ActiveValue::set(first_status),                            second_status: ActiveValue::set(second_status),                            lines_added: ActiveValue::set(                                status_entry.diff_stat_added.map(|v| v as i32),                            ),                            lines_deleted: ActiveValue::set(                                status_entry.diff_stat_deleted.map(|v| v as i32),                            ),                        }                    }),                )                .on_conflict(                    OnConflict::columns([                        project_repository_statuses::Column::ProjectId,                        project_repository_statuses::Column::RepositoryId,                        project_repository_statuses::Column::RepoPath,                    ])                    .update_columns([                        project_repository_statuses::Column::ScanId,                        project_repository_statuses::Column::StatusKind,                        project_repository_statuses::Column::FirstStatus,                        project_repository_statuses::Column::SecondStatus,                        project_repository_statuses::Column::LinesAdded,                        project_repository_statuses::Column::LinesDeleted,                    ])                    .to_owned(),                )                .exec(&*tx)                .await?;            }             let has_any_removed_statuses = !update.removed_statuses.is_empty();             if has_any_removed_statuses {                project_repository_statuses::Entity::update_many()                    .filter(                        project_repository_statuses::Column::ProjectId                            .eq(project_id)                            .and(                                project_repository_statuses::Column::RepositoryId.eq(repository_id),                            )                            .and(                                project_repository_statuses::Column::RepoPath                                    .is_in(update.removed_statuses.iter()),                            ),                    )                    .set(project_repository_statuses::ActiveModel {                        is_deleted: ActiveValue::Set(true),                        scan_id: ActiveValue::Set(update.scan_id as i64),                        ..Default::default()                    })                    .exec(&*tx)                    .await?;            }             let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;            Ok(connection_ids)        })        .await    }     pub async fn remove_repository(        &self,        remove: &proto::RemoveRepository,        _connection: ConnectionId,    ) -> Result<TransactionGuard<Vec<ConnectionId>>> {        let project_id = ProjectId::from_proto(remove.project_id);        let repository_id = remove.id as i64;        self.project_transaction(project_id, |tx| async move {            project_repository::Entity::update_many()                .filter(                    project_repository::Column::ProjectId                        .eq(project_id)                        .and(project_repository::Column::Id.eq(repository_id)),                )                .set(project_repository::ActiveModel {

Lines 1–500 of 1401.