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