db.rs

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