1mod ids;
2pub mod queries;
3mod tables;
4
5use crate::{Error, Result};
6use anyhow::{Context as _, anyhow};
7use collections::{BTreeMap, BTreeSet, HashMap, HashSet};
8use dashmap::DashMap;
9use futures::StreamExt;
10use project_repository_statuses::StatusKind;
11use rpc::ExtensionProvides;
12use rpc::{
13 ConnectionId, ExtensionMetadata,
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 })
536 .collect(),
537 collaborators: self
538 .collaborators
539 .iter()
540 .map(|collaborator| collaborator.to_proto())
541 .collect(),
542 language_servers,
543 language_server_capabilities,
544 }
545 }
546}
547
548#[derive(Debug)]
549pub struct RejoinedWorktree {
550 pub id: u64,
551 pub abs_path: String,
552 pub root_name: String,
553 pub visible: bool,
554 pub updated_entries: Vec<proto::Entry>,
555 pub removed_entries: Vec<u64>,
556 pub updated_repositories: Vec<proto::RepositoryEntry>,
557 pub removed_repositories: Vec<u64>,
558 pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
559 pub settings_files: Vec<WorktreeSettingsFile>,
560 pub scan_id: u64,
561 pub completed_scan_id: u64,
562}
563
564pub struct LeftRoom {
565 pub room: proto::Room,
566 pub channel: Option<channel::Model>,
567 pub left_projects: HashMap<ProjectId, LeftProject>,
568 pub canceled_calls_to_user_ids: Vec<UserId>,
569 pub deleted: bool,
570}
571
572pub struct RefreshedRoom {
573 pub room: proto::Room,
574 pub channel: Option<channel::Model>,
575 pub stale_participant_user_ids: Vec<UserId>,
576 pub canceled_calls_to_user_ids: Vec<UserId>,
577}
578
579pub struct RefreshedChannelBuffer {
580 pub connection_ids: Vec<ConnectionId>,
581 pub collaborators: Vec<proto::Collaborator>,
582}
583
584pub struct Project {
585 pub id: ProjectId,
586 pub role: ChannelRole,
587 pub collaborators: Vec<ProjectCollaborator>,
588 pub worktrees: BTreeMap<u64, Worktree>,
589 pub repositories: Vec<proto::UpdateRepository>,
590 pub language_servers: Vec<LanguageServer>,
591 pub path_style: PathStyle,
592}
593
594pub struct ProjectCollaborator {
595 pub connection_id: ConnectionId,
596 pub user_id: UserId,
597 pub replica_id: ReplicaId,
598 pub is_host: bool,
599 pub committer_name: Option<String>,
600 pub committer_email: Option<String>,
601}
602
603impl ProjectCollaborator {
604 pub fn to_proto(&self) -> proto::Collaborator {
605 proto::Collaborator {
606 peer_id: Some(self.connection_id.into()),
607 replica_id: self.replica_id.0 as u32,
608 user_id: self.user_id.to_proto(),
609 is_host: self.is_host,
610 committer_name: self.committer_name.clone(),
611 committer_email: self.committer_email.clone(),
612 }
613 }
614}
615
616#[derive(Debug, Clone)]
617pub struct LanguageServer {
618 pub server: proto::LanguageServer,
619 pub capabilities: String,
620}
621
622#[derive(Debug)]
623pub struct LeftProject {
624 pub id: ProjectId,
625 pub should_unshare: bool,
626 pub connection_ids: Vec<ConnectionId>,
627}
628
629pub struct Worktree {
630 pub id: u64,
631 pub abs_path: String,
632 pub root_name: String,
633 pub visible: bool,
634 pub entries: Vec<proto::Entry>,
635 pub legacy_repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
636 pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
637 pub settings_files: Vec<WorktreeSettingsFile>,
638 pub scan_id: u64,
639 pub completed_scan_id: u64,
640}
641
642#[derive(Debug)]
643pub struct WorktreeSettingsFile {
644 pub path: String,
645 pub content: String,
646 pub kind: LocalSettingsKind,
647 pub outside_worktree: bool,
648}
649
650pub struct NewExtensionVersion {
651 pub name: String,
652 pub version: semver::Version,
653 pub description: String,
654 pub authors: Vec<String>,
655 pub repository: String,
656 pub schema_version: i32,
657 pub wasm_api_version: Option<String>,
658 pub provides: BTreeSet<ExtensionProvides>,
659 pub published_at: PrimitiveDateTime,
660}
661
662pub struct ExtensionVersionConstraints {
663 pub schema_versions: RangeInclusive<i32>,
664 pub wasm_api_versions: RangeInclusive<semver::Version>,
665}
666
667impl LocalSettingsKind {
668 pub fn from_proto(proto_kind: proto::LocalSettingsKind) -> Self {
669 match proto_kind {
670 proto::LocalSettingsKind::Settings => Self::Settings,
671 proto::LocalSettingsKind::Tasks => Self::Tasks,
672 proto::LocalSettingsKind::Editorconfig => Self::Editorconfig,
673 proto::LocalSettingsKind::Debug => Self::Debug,
674 }
675 }
676
677 pub fn to_proto(self) -> proto::LocalSettingsKind {
678 match self {
679 Self::Settings => proto::LocalSettingsKind::Settings,
680 Self::Tasks => proto::LocalSettingsKind::Tasks,
681 Self::Editorconfig => proto::LocalSettingsKind::Editorconfig,
682 Self::Debug => proto::LocalSettingsKind::Debug,
683 }
684 }
685}
686
687fn db_status_to_proto(
688 entry: project_repository_statuses::Model,
689) -> anyhow::Result<proto::StatusEntry> {
690 use proto::git_file_status::{Tracked, Unmerged, Variant};
691
692 let (simple_status, variant) =
693 match (entry.status_kind, entry.first_status, entry.second_status) {
694 (StatusKind::Untracked, None, None) => (
695 proto::GitStatus::Added as i32,
696 Variant::Untracked(Default::default()),
697 ),
698 (StatusKind::Ignored, None, None) => (
699 proto::GitStatus::Added as i32,
700 Variant::Ignored(Default::default()),
701 ),
702 (StatusKind::Unmerged, Some(first_head), Some(second_head)) => (
703 proto::GitStatus::Conflict as i32,
704 Variant::Unmerged(Unmerged {
705 first_head,
706 second_head,
707 }),
708 ),
709 (StatusKind::Tracked, Some(index_status), Some(worktree_status)) => {
710 let simple_status = if worktree_status != proto::GitStatus::Unmodified as i32 {
711 worktree_status
712 } else if index_status != proto::GitStatus::Unmodified as i32 {
713 index_status
714 } else {
715 proto::GitStatus::Unmodified as i32
716 };
717 (
718 simple_status,
719 Variant::Tracked(Tracked {
720 index_status,
721 worktree_status,
722 }),
723 )
724 }
725 _ => {
726 anyhow::bail!("Unexpected combination of status fields: {entry:?}");
727 }
728 };
729 Ok(proto::StatusEntry {
730 repo_path: entry.repo_path,
731 simple_status,
732 status: Some(proto::GitFileStatus {
733 variant: Some(variant),
734 }),
735 })
736}
737
738fn proto_status_to_db(
739 status_entry: proto::StatusEntry,
740) -> (String, StatusKind, Option<i32>, Option<i32>) {
741 use proto::git_file_status::{Tracked, Unmerged, Variant};
742
743 let (status_kind, first_status, second_status) = status_entry
744 .status
745 .clone()
746 .and_then(|status| status.variant)
747 .map_or(
748 (StatusKind::Untracked, None, None),
749 |variant| match variant {
750 Variant::Untracked(_) => (StatusKind::Untracked, None, None),
751 Variant::Ignored(_) => (StatusKind::Ignored, None, None),
752 Variant::Unmerged(Unmerged {
753 first_head,
754 second_head,
755 }) => (StatusKind::Unmerged, Some(first_head), Some(second_head)),
756 Variant::Tracked(Tracked {
757 index_status,
758 worktree_status,
759 }) => (
760 StatusKind::Tracked,
761 Some(index_status),
762 Some(worktree_status),
763 ),
764 },
765 );
766 (
767 status_entry.repo_path,
768 status_kind,
769 first_status,
770 second_status,
771 )
772}