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