db.rs

  1mod ids;
  2mod queries;
  3mod tables;
  4#[cfg(test)]
  5pub mod tests;
  6
  7use crate::{executor::Executor, Error, Result};
  8use anyhow::anyhow;
  9use collections::{BTreeMap, HashMap, HashSet};
 10use dashmap::DashMap;
 11use futures::StreamExt;
 12use rand::{prelude::StdRng, Rng, SeedableRng};
 13use rpc::{
 14    proto::{self},
 15    ConnectionId, ExtensionMetadata,
 16};
 17use sea_orm::{
 18    entity::prelude::*,
 19    sea_query::{Alias, Expr, OnConflict},
 20    ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr,
 21    FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement,
 22    TransactionTrait,
 23};
 24use semantic_version::SemanticVersion;
 25use serde::{Deserialize, Serialize};
 26use std::ops::RangeInclusive;
 27use std::{
 28    fmt::Write as _,
 29    future::Future,
 30    marker::PhantomData,
 31    ops::{Deref, DerefMut},
 32    rc::Rc,
 33    sync::Arc,
 34    time::Duration,
 35};
 36use time::PrimitiveDateTime;
 37use tokio::sync::{Mutex, OwnedMutexGuard};
 38use worktree_settings_file::LocalSettingsKind;
 39
 40#[cfg(test)]
 41pub use tests::TestDb;
 42
 43pub use ids::*;
 44pub use queries::billing_customers::{CreateBillingCustomerParams, UpdateBillingCustomerParams};
 45pub use queries::billing_preferences::{
 46    CreateBillingPreferencesParams, UpdateBillingPreferencesParams,
 47};
 48pub use queries::billing_subscriptions::{
 49    CreateBillingSubscriptionParams, UpdateBillingSubscriptionParams,
 50};
 51pub use queries::contributors::ContributorSelector;
 52pub use queries::processed_stripe_events::CreateProcessedStripeEventParams;
 53pub use sea_orm::ConnectOptions;
 54pub use tables::user::Model as User;
 55pub use tables::*;
 56
 57/// Database gives you a handle that lets you access the database.
 58/// It handles pooling internally.
 59pub struct Database {
 60    options: ConnectOptions,
 61    pool: DatabaseConnection,
 62    rooms: DashMap<RoomId, Arc<Mutex<()>>>,
 63    projects: DashMap<ProjectId, Arc<Mutex<()>>>,
 64    rng: Mutex<StdRng>,
 65    executor: Executor,
 66    notification_kinds_by_id: HashMap<NotificationKindId, &'static str>,
 67    notification_kinds_by_name: HashMap<String, NotificationKindId>,
 68    #[cfg(test)]
 69    runtime: Option<tokio::runtime::Runtime>,
 70}
 71
 72// The `Database` type has so many methods that its impl blocks are split into
 73// separate files in the `queries` folder.
 74impl Database {
 75    /// Connects to the database with the given options
 76    pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
 77        sqlx::any::install_default_drivers();
 78        Ok(Self {
 79            options: options.clone(),
 80            pool: sea_orm::Database::connect(options).await?,
 81            rooms: DashMap::with_capacity(16384),
 82            projects: DashMap::with_capacity(16384),
 83            rng: Mutex::new(StdRng::seed_from_u64(0)),
 84            notification_kinds_by_id: HashMap::default(),
 85            notification_kinds_by_name: HashMap::default(),
 86            executor,
 87            #[cfg(test)]
 88            runtime: None,
 89        })
 90    }
 91
 92    pub fn options(&self) -> &ConnectOptions {
 93        &self.options
 94    }
 95
 96    #[cfg(test)]
 97    pub fn reset(&self) {
 98        self.rooms.clear();
 99        self.projects.clear();
100    }
101
102    /// Transaction runs things in a transaction. If you want to call other methods
103    /// and pass the transaction around you need to reborrow the transaction at each
104    /// call site with: `&*tx`.
105    pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
106    where
107        F: Send + Fn(TransactionHandle) -> Fut,
108        Fut: Send + Future<Output = Result<T>>,
109    {
110        let body = async {
111            let mut i = 0;
112            loop {
113                let (tx, result) = self.with_transaction(&f).await?;
114                match result {
115                    Ok(result) => match tx.commit().await.map_err(Into::into) {
116                        Ok(()) => return Ok(result),
117                        Err(error) => {
118                            if !self.retry_on_serialization_error(&error, i).await {
119                                return Err(error);
120                            }
121                        }
122                    },
123                    Err(error) => {
124                        tx.rollback().await?;
125                        if !self.retry_on_serialization_error(&error, i).await {
126                            return Err(error);
127                        }
128                    }
129                }
130                i += 1;
131            }
132        };
133
134        self.run(body).await
135    }
136
137    pub async fn weak_transaction<F, Fut, T>(&self, f: F) -> Result<T>
138    where
139        F: Send + Fn(TransactionHandle) -> Fut,
140        Fut: Send + Future<Output = Result<T>>,
141    {
142        let body = async {
143            let (tx, result) = self.with_weak_transaction(&f).await?;
144            match result {
145                Ok(result) => match tx.commit().await.map_err(Into::into) {
146                    Ok(()) => Ok(result),
147                    Err(error) => Err(error),
148                },
149                Err(error) => {
150                    tx.rollback().await?;
151                    Err(error)
152                }
153            }
154        };
155
156        self.run(body).await
157    }
158
159    /// The same as room_transaction, but if you need to only optionally return a Room.
160    async fn optional_room_transaction<F, Fut, T>(
161        &self,
162        f: F,
163    ) -> Result<Option<TransactionGuard<T>>>
164    where
165        F: Send + Fn(TransactionHandle) -> Fut,
166        Fut: Send + Future<Output = Result<Option<(RoomId, T)>>>,
167    {
168        let body = async {
169            let mut i = 0;
170            loop {
171                let (tx, result) = self.with_transaction(&f).await?;
172                match result {
173                    Ok(Some((room_id, data))) => {
174                        let lock = self.rooms.entry(room_id).or_default().clone();
175                        let _guard = lock.lock_owned().await;
176                        match tx.commit().await.map_err(Into::into) {
177                            Ok(()) => {
178                                return Ok(Some(TransactionGuard {
179                                    data,
180                                    _guard,
181                                    _not_send: PhantomData,
182                                }));
183                            }
184                            Err(error) => {
185                                if !self.retry_on_serialization_error(&error, i).await {
186                                    return Err(error);
187                                }
188                            }
189                        }
190                    }
191                    Ok(None) => match tx.commit().await.map_err(Into::into) {
192                        Ok(()) => return Ok(None),
193                        Err(error) => {
194                            if !self.retry_on_serialization_error(&error, i).await {
195                                return Err(error);
196                            }
197                        }
198                    },
199                    Err(error) => {
200                        tx.rollback().await?;
201                        if !self.retry_on_serialization_error(&error, i).await {
202                            return Err(error);
203                        }
204                    }
205                }
206                i += 1;
207            }
208        };
209
210        self.run(body).await
211    }
212
213    async fn project_transaction<F, Fut, T>(
214        &self,
215        project_id: ProjectId,
216        f: F,
217    ) -> Result<TransactionGuard<T>>
218    where
219        F: Send + Fn(TransactionHandle) -> Fut,
220        Fut: Send + Future<Output = Result<T>>,
221    {
222        let room_id = Database::room_id_for_project(self, project_id).await?;
223        let body = async {
224            let mut i = 0;
225            loop {
226                let lock = if let Some(room_id) = room_id {
227                    self.rooms.entry(room_id).or_default().clone()
228                } else {
229                    self.projects.entry(project_id).or_default().clone()
230                };
231                let _guard = lock.lock_owned().await;
232                let (tx, result) = self.with_transaction(&f).await?;
233                match result {
234                    Ok(data) => match tx.commit().await.map_err(Into::into) {
235                        Ok(()) => {
236                            return Ok(TransactionGuard {
237                                data,
238                                _guard,
239                                _not_send: PhantomData,
240                            });
241                        }
242                        Err(error) => {
243                            if !self.retry_on_serialization_error(&error, i).await {
244                                return Err(error);
245                            }
246                        }
247                    },
248                    Err(error) => {
249                        tx.rollback().await?;
250                        if !self.retry_on_serialization_error(&error, i).await {
251                            return Err(error);
252                        }
253                    }
254                }
255                i += 1;
256            }
257        };
258
259        self.run(body).await
260    }
261
262    /// room_transaction runs the block in a transaction. It returns a RoomGuard, that keeps
263    /// the database locked until it is dropped. This ensures that updates sent to clients are
264    /// properly serialized with respect to database changes.
265    async fn room_transaction<F, Fut, T>(
266        &self,
267        room_id: RoomId,
268        f: F,
269    ) -> Result<TransactionGuard<T>>
270    where
271        F: Send + Fn(TransactionHandle) -> Fut,
272        Fut: Send + Future<Output = Result<T>>,
273    {
274        let body = async {
275            let mut i = 0;
276            loop {
277                let lock = self.rooms.entry(room_id).or_default().clone();
278                let _guard = lock.lock_owned().await;
279                let (tx, result) = self.with_transaction(&f).await?;
280                match result {
281                    Ok(data) => match tx.commit().await.map_err(Into::into) {
282                        Ok(()) => {
283                            return Ok(TransactionGuard {
284                                data,
285                                _guard,
286                                _not_send: PhantomData,
287                            });
288                        }
289                        Err(error) => {
290                            if !self.retry_on_serialization_error(&error, i).await {
291                                return Err(error);
292                            }
293                        }
294                    },
295                    Err(error) => {
296                        tx.rollback().await?;
297                        if !self.retry_on_serialization_error(&error, i).await {
298                            return Err(error);
299                        }
300                    }
301                }
302                i += 1;
303            }
304        };
305
306        self.run(body).await
307    }
308
309    async fn with_transaction<F, Fut, T>(&self, f: &F) -> Result<(DatabaseTransaction, Result<T>)>
310    where
311        F: Send + Fn(TransactionHandle) -> Fut,
312        Fut: Send + Future<Output = Result<T>>,
313    {
314        let tx = self
315            .pool
316            .begin_with_config(Some(IsolationLevel::Serializable), None)
317            .await?;
318
319        let mut tx = Arc::new(Some(tx));
320        let result = f(TransactionHandle(tx.clone())).await;
321        let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else {
322            return Err(anyhow!(
323                "couldn't complete transaction because it's still in use"
324            ))?;
325        };
326
327        Ok((tx, result))
328    }
329
330    async fn with_weak_transaction<F, Fut, T>(
331        &self,
332        f: &F,
333    ) -> Result<(DatabaseTransaction, Result<T>)>
334    where
335        F: Send + Fn(TransactionHandle) -> Fut,
336        Fut: Send + Future<Output = Result<T>>,
337    {
338        let tx = self
339            .pool
340            .begin_with_config(Some(IsolationLevel::ReadCommitted), None)
341            .await?;
342
343        let mut tx = Arc::new(Some(tx));
344        let result = f(TransactionHandle(tx.clone())).await;
345        let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else {
346            return Err(anyhow!(
347                "couldn't complete transaction because it's still in use"
348            ))?;
349        };
350
351        Ok((tx, result))
352    }
353
354    async fn run<F, T>(&self, future: F) -> Result<T>
355    where
356        F: Future<Output = Result<T>>,
357    {
358        #[cfg(test)]
359        {
360            if let Executor::Deterministic(executor) = &self.executor {
361                executor.simulate_random_delay().await;
362            }
363
364            self.runtime.as_ref().unwrap().block_on(future)
365        }
366
367        #[cfg(not(test))]
368        {
369            future.await
370        }
371    }
372
373    async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: usize) -> bool {
374        // If the error is due to a failure to serialize concurrent transactions, then retry
375        // this transaction after a delay. With each subsequent retry, double the delay duration.
376        // Also vary the delay randomly in order to ensure different database connections retry
377        // at different times.
378        const SLEEPS: [f32; 10] = [10., 20., 40., 80., 160., 320., 640., 1280., 2560., 5120.];
379        if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
380            let base_delay = SLEEPS[prev_attempt_count];
381            let randomized_delay = base_delay * self.rng.lock().await.gen_range(0.5..=2.0);
382            log::warn!(
383                "retrying transaction after serialization error. delay: {} ms.",
384                randomized_delay
385            );
386            self.executor
387                .sleep(Duration::from_millis(randomized_delay as u64))
388                .await;
389            true
390        } else {
391            false
392        }
393    }
394}
395
396fn is_serialization_error(error: &Error) -> bool {
397    const SERIALIZATION_FAILURE_CODE: &str = "40001";
398    match error {
399        Error::Database(
400            DbErr::Exec(sea_orm::RuntimeErr::SqlxError(error))
401            | DbErr::Query(sea_orm::RuntimeErr::SqlxError(error)),
402        ) if error
403            .as_database_error()
404            .and_then(|error| error.code())
405            .as_deref()
406            == Some(SERIALIZATION_FAILURE_CODE) =>
407        {
408            true
409        }
410        _ => false,
411    }
412}
413
414/// A handle to a [`DatabaseTransaction`].
415pub struct TransactionHandle(pub(crate) Arc<Option<DatabaseTransaction>>);
416
417impl Deref for TransactionHandle {
418    type Target = DatabaseTransaction;
419
420    fn deref(&self) -> &Self::Target {
421        self.0.as_ref().as_ref().unwrap()
422    }
423}
424
425/// [`TransactionGuard`] keeps a database transaction alive until it is dropped.
426/// It wraps data that depends on the state of the database and prevents an additional
427/// transaction from starting that would invalidate that data.
428pub struct TransactionGuard<T> {
429    data: T,
430    _guard: OwnedMutexGuard<()>,
431    _not_send: PhantomData<Rc<()>>,
432}
433
434impl<T> Deref for TransactionGuard<T> {
435    type Target = T;
436
437    fn deref(&self) -> &T {
438        &self.data
439    }
440}
441
442impl<T> DerefMut for TransactionGuard<T> {
443    fn deref_mut(&mut self) -> &mut T {
444        &mut self.data
445    }
446}
447
448impl<T> TransactionGuard<T> {
449    /// Returns the inner value of the guard.
450    pub fn into_inner(self) -> T {
451        self.data
452    }
453}
454
455#[derive(Clone, Debug, PartialEq, Eq)]
456pub enum Contact {
457    Accepted { user_id: UserId, busy: bool },
458    Outgoing { user_id: UserId },
459    Incoming { user_id: UserId },
460}
461
462impl Contact {
463    pub fn user_id(&self) -> UserId {
464        match self {
465            Contact::Accepted { user_id, .. } => *user_id,
466            Contact::Outgoing { user_id } => *user_id,
467            Contact::Incoming { user_id, .. } => *user_id,
468        }
469    }
470}
471
472pub type NotificationBatch = Vec<(UserId, proto::Notification)>;
473
474pub struct CreatedChannelMessage {
475    pub message_id: MessageId,
476    pub participant_connection_ids: HashSet<ConnectionId>,
477    pub notifications: NotificationBatch,
478}
479
480pub struct UpdatedChannelMessage {
481    pub message_id: MessageId,
482    pub participant_connection_ids: Vec<ConnectionId>,
483    pub notifications: NotificationBatch,
484    pub reply_to_message_id: Option<MessageId>,
485    pub timestamp: PrimitiveDateTime,
486    pub deleted_mention_notification_ids: Vec<NotificationId>,
487    pub updated_mention_notifications: Vec<rpc::proto::Notification>,
488}
489
490#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)]
491pub struct Invite {
492    pub email_address: String,
493    pub email_confirmation_code: String,
494}
495
496#[derive(Clone, Debug, Deserialize)]
497pub struct NewSignup {
498    pub email_address: String,
499    pub platform_mac: bool,
500    pub platform_windows: bool,
501    pub platform_linux: bool,
502    pub editor_features: Vec<String>,
503    pub programming_languages: Vec<String>,
504    pub device_id: Option<String>,
505    pub added_to_mailing_list: bool,
506    pub created_at: Option<DateTime>,
507}
508
509#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)]
510pub struct WaitlistSummary {
511    pub count: i64,
512    pub linux_count: i64,
513    pub mac_count: i64,
514    pub windows_count: i64,
515    pub unknown_count: i64,
516}
517
518/// The parameters to create a new user.
519#[derive(Debug, Serialize, Deserialize)]
520pub struct NewUserParams {
521    pub github_login: String,
522    pub github_user_id: i32,
523}
524
525/// The result of creating a new user.
526#[derive(Debug)]
527pub struct NewUserResult {
528    pub user_id: UserId,
529    pub metrics_id: String,
530    pub inviting_user_id: Option<UserId>,
531    pub signup_device_id: Option<String>,
532}
533
534/// The result of updating a channel membership.
535#[derive(Debug)]
536pub struct MembershipUpdated {
537    pub channel_id: ChannelId,
538    pub new_channels: ChannelsForUser,
539    pub removed_channels: Vec<ChannelId>,
540}
541
542/// The result of setting a member's role.
543#[derive(Debug)]
544#[allow(clippy::large_enum_variant)]
545pub enum SetMemberRoleResult {
546    InviteUpdated(Channel),
547    MembershipUpdated(MembershipUpdated),
548}
549
550/// The result of inviting a member to a channel.
551#[derive(Debug)]
552pub struct InviteMemberResult {
553    pub channel: Channel,
554    pub notifications: NotificationBatch,
555}
556
557#[derive(Debug)]
558pub struct RespondToChannelInvite {
559    pub membership_update: Option<MembershipUpdated>,
560    pub notifications: NotificationBatch,
561}
562
563#[derive(Debug)]
564pub struct RemoveChannelMemberResult {
565    pub membership_update: MembershipUpdated,
566    pub notification_id: Option<NotificationId>,
567}
568
569#[derive(Debug, PartialEq, Eq, Hash)]
570pub struct Channel {
571    pub id: ChannelId,
572    pub name: String,
573    pub visibility: ChannelVisibility,
574    /// parent_path is the channel ids from the root to this one (not including this one)
575    pub parent_path: Vec<ChannelId>,
576}
577
578impl Channel {
579    pub fn from_model(value: channel::Model) -> Self {
580        Channel {
581            id: value.id,
582            visibility: value.visibility,
583            name: value.clone().name,
584            parent_path: value.ancestors().collect(),
585        }
586    }
587
588    pub fn to_proto(&self) -> proto::Channel {
589        proto::Channel {
590            id: self.id.to_proto(),
591            name: self.name.clone(),
592            visibility: self.visibility.into(),
593            parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(),
594        }
595    }
596}
597
598#[derive(Debug, PartialEq, Eq, Hash)]
599pub struct ChannelMember {
600    pub role: ChannelRole,
601    pub user_id: UserId,
602    pub kind: proto::channel_member::Kind,
603}
604
605impl ChannelMember {
606    pub fn to_proto(&self) -> proto::ChannelMember {
607        proto::ChannelMember {
608            role: self.role.into(),
609            user_id: self.user_id.to_proto(),
610            kind: self.kind.into(),
611        }
612    }
613}
614
615#[derive(Debug, PartialEq)]
616pub struct ChannelsForUser {
617    pub channels: Vec<Channel>,
618    pub channel_memberships: Vec<channel_member::Model>,
619    pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
620    pub invited_channels: Vec<Channel>,
621
622    pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
623    pub observed_channel_messages: Vec<proto::ChannelMessageId>,
624    pub latest_buffer_versions: Vec<proto::ChannelBufferVersion>,
625    pub latest_channel_messages: Vec<proto::ChannelMessageId>,
626}
627
628#[derive(Debug)]
629pub struct RejoinedChannelBuffer {
630    pub buffer: proto::RejoinedChannelBuffer,
631    pub old_connection_id: ConnectionId,
632}
633
634#[derive(Clone)]
635pub struct JoinRoom {
636    pub room: proto::Room,
637    pub channel: Option<channel::Model>,
638}
639
640pub struct RejoinedRoom {
641    pub room: proto::Room,
642    pub rejoined_projects: Vec<RejoinedProject>,
643    pub reshared_projects: Vec<ResharedProject>,
644    pub channel: Option<channel::Model>,
645}
646
647pub struct ResharedProject {
648    pub id: ProjectId,
649    pub old_connection_id: ConnectionId,
650    pub collaborators: Vec<ProjectCollaborator>,
651    pub worktrees: Vec<proto::WorktreeMetadata>,
652}
653
654pub struct RejoinedProject {
655    pub id: ProjectId,
656    pub old_connection_id: ConnectionId,
657    pub collaborators: Vec<ProjectCollaborator>,
658    pub worktrees: Vec<RejoinedWorktree>,
659    pub language_servers: Vec<proto::LanguageServer>,
660}
661
662impl RejoinedProject {
663    pub fn to_proto(&self) -> proto::RejoinedProject {
664        proto::RejoinedProject {
665            id: self.id.to_proto(),
666            worktrees: self
667                .worktrees
668                .iter()
669                .map(|worktree| proto::WorktreeMetadata {
670                    id: worktree.id,
671                    root_name: worktree.root_name.clone(),
672                    visible: worktree.visible,
673                    abs_path: worktree.abs_path.clone(),
674                })
675                .collect(),
676            collaborators: self
677                .collaborators
678                .iter()
679                .map(|collaborator| collaborator.to_proto())
680                .collect(),
681            language_servers: self.language_servers.clone(),
682        }
683    }
684}
685
686#[derive(Debug)]
687pub struct RejoinedWorktree {
688    pub id: u64,
689    pub abs_path: String,
690    pub root_name: String,
691    pub visible: bool,
692    pub updated_entries: Vec<proto::Entry>,
693    pub removed_entries: Vec<u64>,
694    pub updated_repositories: Vec<proto::RepositoryEntry>,
695    pub removed_repositories: Vec<u64>,
696    pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
697    pub settings_files: Vec<WorktreeSettingsFile>,
698    pub scan_id: u64,
699    pub completed_scan_id: u64,
700}
701
702pub struct LeftRoom {
703    pub room: proto::Room,
704    pub channel: Option<channel::Model>,
705    pub left_projects: HashMap<ProjectId, LeftProject>,
706    pub canceled_calls_to_user_ids: Vec<UserId>,
707    pub deleted: bool,
708}
709
710pub struct RefreshedRoom {
711    pub room: proto::Room,
712    pub channel: Option<channel::Model>,
713    pub stale_participant_user_ids: Vec<UserId>,
714    pub canceled_calls_to_user_ids: Vec<UserId>,
715}
716
717pub struct RefreshedChannelBuffer {
718    pub connection_ids: Vec<ConnectionId>,
719    pub collaborators: Vec<proto::Collaborator>,
720}
721
722pub struct Project {
723    pub id: ProjectId,
724    pub role: ChannelRole,
725    pub collaborators: Vec<ProjectCollaborator>,
726    pub worktrees: BTreeMap<u64, Worktree>,
727    pub language_servers: Vec<proto::LanguageServer>,
728}
729
730pub struct ProjectCollaborator {
731    pub connection_id: ConnectionId,
732    pub user_id: UserId,
733    pub replica_id: ReplicaId,
734    pub is_host: bool,
735}
736
737impl ProjectCollaborator {
738    pub fn to_proto(&self) -> proto::Collaborator {
739        proto::Collaborator {
740            peer_id: Some(self.connection_id.into()),
741            replica_id: self.replica_id.0 as u32,
742            user_id: self.user_id.to_proto(),
743            is_host: self.is_host,
744        }
745    }
746}
747
748#[derive(Debug)]
749pub struct LeftProject {
750    pub id: ProjectId,
751    pub should_unshare: bool,
752    pub connection_ids: Vec<ConnectionId>,
753}
754
755pub struct Worktree {
756    pub id: u64,
757    pub abs_path: String,
758    pub root_name: String,
759    pub visible: bool,
760    pub entries: Vec<proto::Entry>,
761    pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
762    pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
763    pub settings_files: Vec<WorktreeSettingsFile>,
764    pub scan_id: u64,
765    pub completed_scan_id: u64,
766}
767
768#[derive(Debug)]
769pub struct WorktreeSettingsFile {
770    pub path: String,
771    pub content: String,
772    pub kind: LocalSettingsKind,
773}
774
775pub struct NewExtensionVersion {
776    pub name: String,
777    pub version: semver::Version,
778    pub description: String,
779    pub authors: Vec<String>,
780    pub repository: String,
781    pub schema_version: i32,
782    pub wasm_api_version: Option<String>,
783    pub published_at: PrimitiveDateTime,
784}
785
786pub struct ExtensionVersionConstraints {
787    pub schema_versions: RangeInclusive<i32>,
788    pub wasm_api_versions: RangeInclusive<SemanticVersion>,
789}
790
791impl LocalSettingsKind {
792    pub fn from_proto(proto_kind: proto::LocalSettingsKind) -> Self {
793        match proto_kind {
794            proto::LocalSettingsKind::Settings => Self::Settings,
795            proto::LocalSettingsKind::Tasks => Self::Tasks,
796            proto::LocalSettingsKind::Editorconfig => Self::Editorconfig,
797        }
798    }
799
800    pub fn to_proto(&self) -> proto::LocalSettingsKind {
801        match self {
802            Self::Settings => proto::LocalSettingsKind::Settings,
803            Self::Tasks => proto::LocalSettingsKind::Tasks,
804            Self::Editorconfig => proto::LocalSettingsKind::Editorconfig,
805        }
806    }
807}