Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:47:26.286Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

git_store.rs

10983 lines · 426.5 KB · rust
1mod conflict_set;
2pub mod diff_buffer_list;
3pub mod git_traversal;
4pub mod job_debug_queue;
5pub mod pending_op;
6
7use crate::{
8    ProjectEnvironment, ProjectItem, ProjectPath,
9    buffer_store::{BufferStore, BufferStoreEvent},
10    project_settings::ProjectSettings,
11    trusted_worktrees::{
12        PathTrust, TrustedWorktrees, TrustedWorktreesEvent, TrustedWorktreesStore,
13    },
14    worktree_store::{WorktreeStore, WorktreeStoreEvent},
15};
16use anyhow::{Context as _, Result, anyhow, bail};
17use askpass::{AskPassDelegate, EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
18use buffer_diff::{BufferDiff, DiffHunk, DiffHunkSecondaryStatus, PendingHunk, PendingSense};
19use client::ProjectId;
20use collections::HashMap;
21pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate};
22use fs::{Fs, RemoveOptions};
23use futures::{
24    FutureExt, SinkExt, Stream, StreamExt,
25    channel::{
26        mpsc,
27        oneshot::{self, Canceled},
28    },
29    future::{self, BoxFuture, Shared},
30    stream::{FuturesOrdered, FuturesUnordered},
31};
32use git::{
33    BuildPermalinkParams, GitHostingProviderRegistry, Oid, RunHook,
34    blame::Blame,
35    parse_git_remote_url,
36    repository::{
37        Branch, BranchesScanResult, CommitData, CommitDetails, CommitDiff, CommitFile,
38        CommitOptions, CreateWorktreeTarget, DiffStatType, DiffType, FetchOptions,
39        FileHistoryChangedFileSets, GitCommitTemplate, GitRepository, GitRepositoryCheckpoint,
40        InitialGraphCommitData, LogOrder, LogSource, PushOptions, Remote, RemoteCommandOutput,
41        RepoPath, ResetMode, SearchCommitArgs, UpstreamTrackingStatus, Worktree as GitWorktree,
42        delete_branch_flag,
43    },
44    stash::{GitStash, StashEntry},
45    status::{
46        self, DiffStat, DiffTreeType, FileStatus, GitSummary, StatusCode, TrackedStatus, TreeDiff,
47        TreeDiffStatus, UnmergedStatus, UnmergedStatusCode,
48    },
49};
50use gpui::{
51    App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString,
52    Subscription, Task, TaskExt, WeakEntity,
53};
54use language::{
55    Anchor, Buffer, BufferEvent, Capability, Language, LanguageRegistry,
56    proto::{deserialize_version, serialize_version},
57};
58use parking_lot::Mutex;
59use paths::{config_dir, home_dir};
60use pending_op::{PendingOp, PendingOpId, PendingOps, PendingOpsSummary};
61use postage::stream::Stream as _;
62use rpc::{
63    AnyProtoClient, TypedEnvelope,
64    proto::{self, git_reset, split_repository_update},
65};
66use serde::Deserialize;
67use settings::{Settings, WorktreeId};
68use smallvec::SmallVec;
69use smol::future::yield_now;
70use std::{
71    cmp::Ordering,
72    collections::{BTreeSet, HashSet, VecDeque, hash_map::Entry},
73    future::Future,
74    mem,
75    ops::Range,
76    path::{Path, PathBuf},
77    str::FromStr,
78    sync::{
79        Arc,
80        atomic::{self, AtomicU64},
81    },
82    time::{Duration, Instant, SystemTime},
83};
84use sum_tree::{Edit, SumTree, TreeMap};
85use task::Shell;
86use text::{Bias, BufferId, OffsetRangeExt, Rope, ToOffset};
87use util::{
88    ResultExt, debug_panic,
89    paths::{PathStyle, SanitizedPath},
90    post_inc,
91    rel_path::RelPath,
92};
93use worktree::{
94    File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId,
95    UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree,
96};
97use zeroize::Zeroize;
98
99pub struct GitStore {
100    state: GitStoreState,
101    buffer_store: Entity<BufferStore>,
102    worktree_store: Entity<WorktreeStore>,
103    repositories: HashMap<RepositoryId, Entity<Repository>>,
104    worktree_ids: HashMap<RepositoryId, HashSet<WorktreeId>>,
105    active_repo_id: Option<RepositoryId>,
106    #[allow(clippy::type_complexity)]
107    loading_diffs:
108        HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
109    diffs: HashMap<BufferId, Entity<BufferGitState>>,
110    buffer_ids_by_index_text_buffer_id: HashMap<BufferId, BufferId>,
111    shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
112    _subscriptions: Vec<Subscription>,
113}
114
115#[derive(Default)]
116struct SharedDiffs {
117    unstaged: Option<Entity<BufferDiff>>,
118    uncommitted: Option<Entity<BufferDiff>>,
119}
120
121struct BufferGitState {
122    unstaged_diff: Option<WeakEntity<BufferDiff>>,
123    staged_diff: Option<(WeakEntity<BufferDiff>, Entity<Buffer>)>,
124    uncommitted_diff: Option<WeakEntity<BufferDiff>>,
125    oid_diffs: HashMap<Option<git::Oid>, WeakEntity<BufferDiff>>,
126    conflict_set: Option<WeakEntity<ConflictSet>>,
127    recalculate_diff_task: Option<Task<Result<()>>>,
128    reparse_conflict_markers_task: Option<Task<Result<()>>>,
129    language: Option<Arc<Language>>,
130    language_registry: Option<Arc<LanguageRegistry>>,
131    conflict_updated_futures: Vec<oneshot::Sender<()>>,
132    recalculating_tx: postage::watch::Sender<bool>,
133
134    /// These operation counts are used to ensure that head and index text
135    /// values read from the git repository are up-to-date with any hunk staging
136    /// operations that have been performed on the BufferDiff.
137    ///
138    /// The operation count is incremented immediately when the user initiates a
139    /// hunk stage/unstage operation. Then, upon finishing writing the new index
140    /// text do disk, the `operation count as of write` is updated to reflect
141    /// the operation count that prompted the write.
142    hunk_staging_operation_count: usize,
143    hunk_staging_operation_count_as_of_write: usize,
144
145    head_text: Option<Arc<str>>,
146    index_text: Option<Arc<str>>,
147    /// The optimistic, in-flight index state: the sole input to the index write,
148    /// expressed relative to the currently-loaded index text (`I0`). Never shown
149    /// to any view (views use per-diff pending hunks). Cleared when a
150    /// recalculation settles.
151    ///
152    /// `I0` is immutable for the lifetime of any pending edit (the index text
153    /// buffer is only fast-forwarded when a recalculation settles, which clears
154    /// this state in the same update), so byte offsets stay valid for the whole
155    /// window.
156    pending_index_edits: Option<Vec<(Range<usize>, Arc<str>)>>,
157    oid_texts: HashMap<git::Oid, Arc<str>>,
158    head_text_buffer: WeakEntity<Buffer>,
159    index_text_buffer: WeakEntity<Buffer>,
160    index_text_buffer_language_enabled: bool,
161    head_changed: bool,
162    index_changed: bool,
163    language_changed: bool,
164}
165
166fn pending_hunks(
167    hunks: &[DiffHunk],
168    version: &clock::Global,
169    sense: PendingSense,
170) -> Vec<PendingHunk> {
171    hunks
172        .iter()
173        .map(|hunk| {
174            PendingHunk::new(
175                hunk.buffer_range.clone(),
176                hunk.diff_base_byte_range.clone(),
177                version.clone(),
178                sense,
179            )
180        })
181        .collect()
182}
183
184#[derive(Clone, Debug)]
185enum DiffBasesChange {
186    SetIndex(Option<String>),
187    SetHead(Option<String>),
188    SetEach {
189        index: Option<String>,
190        head: Option<String>,
191    },
192    SetBoth(Option<String>),
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
196enum DiffKind {
197    Unstaged,
198    Staged,
199    Uncommitted,
200    SinceOid(Option<git::Oid>),
201}
202
203struct IndexTextFile {
204    path: Arc<RelPath>,
205    full_path: PathBuf,
206    path_style: PathStyle,
207    file_name: String,
208    worktree_id: WorktreeId,
209    is_private: bool,
210}
211
212impl IndexTextFile {
213    fn new(file: &dyn language::File, cx: &App) -> Self {
214        Self {
215            path: file.path().clone(),
216            full_path: file.full_path(cx),
217            path_style: file.path_style(cx),
218            file_name: file.file_name(cx).to_string(),
219            worktree_id: file.worktree_id(cx),
220            is_private: file.is_private(),
221        }
222    }
223}
224
225impl language::File for IndexTextFile {
226    fn as_local(&self) -> Option<&dyn language::LocalFile> {
227        None
228    }
229
230    fn disk_state(&self) -> language::DiskState {
231        language::DiskState::Historic { was_deleted: false }
232    }
233
234    fn path(&self) -> &Arc<RelPath> {
235        &self.path
236    }
237
238    fn full_path(&self, _: &App) -> PathBuf {
239        self.full_path.clone()
240    }
241
242    fn path_style(&self, _: &App) -> PathStyle {
243        self.path_style
244    }
245
246    fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
247        &self.file_name
248    }
249
250    fn worktree_id(&self, _: &App) -> WorktreeId {
251        self.worktree_id
252    }
253
254    fn to_proto(&self, _: &App) -> rpc::proto::File {
255        rpc::proto::File {
256            worktree_id: self.worktree_id.to_proto(),
257            entry_id: None,
258            path: self.path.as_ref().as_unix_str().to_owned(),
259            mtime: None,
260            is_deleted: false,
261            is_historic: true,
262        }
263    }
264
265    fn is_private(&self) -> bool {
266        self.is_private
267    }
268}
269
270#[derive(Debug, Clone, Copy)]
271pub enum GitAccess {
272    /// Either:
273    /// - the user owns `.git`
274    /// - the user doesn't own `.git`, but has both of:
275    ///   - OS-level read permissions
276    ///   - the directory is marked as safe (git config safe.directory)
277    Yes,
278
279    /// The user is not the owner of `.git`, and one of the following is true:
280    /// - the directory is not marked as safe (git config safe.directory)
281    /// - the user does not have OS-level read permissions to `.git`
282    No,
283}
284
285enum GitStoreState {
286    Local {
287        next_repository_id: Arc<AtomicU64>,
288        downstream: Option<LocalDownstreamState>,
289        project_environment: Entity<ProjectEnvironment>,
290        fs: Arc<dyn Fs>,
291        _fs_watches: Box<[Task<()>]>,
292    },
293    Remote {
294        upstream_client: AnyProtoClient,
295        upstream_project_id: u64,
296        downstream: Option<(AnyProtoClient, ProjectId)>,
297    },
298}
299
300enum DownstreamUpdate {
301    UpdateRepository(RepositorySnapshot),
302    RemoveRepository(RepositoryId),
303}
304
305struct LocalDownstreamState {
306    client: AnyProtoClient,
307    project_id: ProjectId,
308    updates_tx: mpsc::UnboundedSender<DownstreamUpdate>,
309    _task: Task<Result<()>>,
310}
311
312#[derive(Clone, Debug)]
313pub struct GitStoreCheckpoint {
314    checkpoints_by_work_dir_abs_path: HashMap<Arc<Path>, GitRepositoryCheckpoint>,
315}
316
317#[derive(Clone, Debug, PartialEq, Eq)]
318pub struct StatusEntry {
319    pub repo_path: RepoPath,
320    pub status: FileStatus,
321    pub diff_stat: Option<DiffStat>,
322    pub staged_diff_stat: Option<DiffStat>,
323    pub unstaged_diff_stat: Option<DiffStat>,
324}
325
326impl StatusEntry {
327    fn to_proto(&self) -> proto::StatusEntry {
328        let simple_status = match self.status {
329            FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32,
330            FileStatus::Unmerged { .. } => proto::GitStatus::Conflict as i32,
331            FileStatus::Tracked(TrackedStatus {
332                index_status,
333                worktree_status,
334            }) => tracked_status_to_proto(if worktree_status != StatusCode::Unmodified {
335                worktree_status
336            } else {
337                index_status
338            }),
339        };
340
341        proto::StatusEntry {
342            repo_path: self.repo_path.as_unix_str().to_owned(),
343            simple_status,
344            status: Some(status_to_proto(self.status)),
345            diff_stat_added: self.diff_stat.map(|ds| ds.added),
346            diff_stat_deleted: self.diff_stat.map(|ds| ds.deleted),
347            staged_diff_stat_added: self.staged_diff_stat.map(|ds| ds.added),
348            staged_diff_stat_deleted: self.staged_diff_stat.map(|ds| ds.deleted),
349            unstaged_diff_stat_added: self.unstaged_diff_stat.map(|ds| ds.added),
350            unstaged_diff_stat_deleted: self.unstaged_diff_stat.map(|ds| ds.deleted),
351        }
352    }
353}
354
355impl TryFrom<proto::StatusEntry> for StatusEntry {
356    type Error = anyhow::Error;
357
358    fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
359        let repo_path = RepoPath::from_proto(&value.repo_path).context("invalid repo path")?;
360        let status = status_from_proto(value.simple_status, value.status)?;
361        let diff_stat = match (value.diff_stat_added, value.diff_stat_deleted) {
362            (Some(added), Some(deleted)) => Some(DiffStat { added, deleted }),
363            _ => None,
364        };
365        let staged_diff_stat = match (value.staged_diff_stat_added, value.staged_diff_stat_deleted)
366        {
367            (Some(added), Some(deleted)) => Some(DiffStat { added, deleted }),
368            _ => None,
369        };
370        let unstaged_diff_stat = match (
371            value.unstaged_diff_stat_added,
372            value.unstaged_diff_stat_deleted,
373        ) {
374            (Some(added), Some(deleted)) => Some(DiffStat { added, deleted }),
375            _ => None,
376        };
377        Ok(Self {
378            repo_path,
379            status,
380            diff_stat,
381            staged_diff_stat,
382            unstaged_diff_stat,
383        })
384    }
385}
386
387impl sum_tree::Item for StatusEntry {
388    type Summary = PathSummary<GitSummary>;
389
390    fn summary(&self, _: <Self::Summary as sum_tree::Summary>::Context<'_>) -> Self::Summary {
391        PathSummary {
392            max_path: self.repo_path.as_ref().clone(),
393            item_summary: self.status.summary(),
394        }
395    }
396}
397
398impl sum_tree::KeyedItem for StatusEntry {
399    type Key = PathKey;
400
401    fn key(&self) -> Self::Key {
402        PathKey(self.repo_path.as_ref().clone())
403    }
404}
405
406#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
407pub struct RepositoryId(pub u64);
408
409#[derive(Clone, Debug, Default, PartialEq, Eq)]
410pub struct MergeDetails {
411    pub merge_heads_by_conflicted_path: TreeMap<RepoPath, Vec<Option<SharedString>>>,
412    pub message: Option<SharedString>,
413}
414
415#[derive(Clone)]
416pub enum CommitDataState {
417    Loading(Option<Shared<oneshot::Receiver<Arc<CommitData>>>>),
418    Loaded(Arc<CommitData>),
419}
420
421#[derive(Clone, Debug, PartialEq, Eq)]
422pub struct RepositorySnapshot {
423    pub id: RepositoryId,
424    pub statuses_by_path: SumTree<StatusEntry>,
425    pub work_directory_abs_path: Arc<Path>,
426    pub dot_git_abs_path: Arc<Path>,
427    /// Absolute path to the directory holding this worktree's Git state.
428    ///
429    /// For a linked worktree this is the worktree-specific directory under the
430    /// common Git directory, such as `<main>/.git/worktrees/<name>`.
431    pub repository_dir_abs_path: Arc<Path>,
432    /// Absolute path to the repository's common Git directory.
433    ///
434    /// For a normal checkout this is `<work_directory>/.git`. For a linked
435    /// worktree this is the common Git directory shared by all worktrees. If
436    /// that common directory is a bare repository, there may be no main
437    /// worktree path to derive from it.
438    pub common_dir_abs_path: Arc<Path>,
439    pub path_style: PathStyle,
440    pub branch: Option<Branch>,
441    pub branch_list: Arc<[Branch]>,
442    pub branch_list_error: Option<SharedString>,
443    pub head_commit: Option<CommitDetails>,
444    pub scan_id: u64,
445    pub merge: MergeDetails,
446    pub remote_origin_url: Option<String>,
447    pub remote_upstream_url: Option<String>,
448    pub stash_entries: GitStash,
449    pub linked_worktrees: Arc<[GitWorktree]>,
450}
451
452type JobId = u64;
453
454#[derive(Clone, Debug, PartialEq, Eq)]
455pub struct JobInfo {
456    pub start: Instant,
457    pub message: SharedString,
458}
459
460struct CommitDataHandler {
461    _task: Task<()>,
462    commit_data_request: async_channel::Sender<Oid>,
463    completion_senders: HashMap<Oid, oneshot::Sender<Arc<CommitData>>>,
464    pending_requests: HashSet<Oid>,
465}
466
467/// Represents the handler of a git cat-file --batch process within Zed
468/// It's used to lazily fetch commit data as needed (whatever a user is viewing)
469enum CommitDataHandlerState {
470    /// The handler is open and processing requests
471    Open(CommitDataHandler),
472    /// The handler closed because it didn't receive any requests in the last 10s
473    /// or hasn't been open before
474    Closed,
475}
476
477enum NextCommitDataRequest {
478    Request(BoxFuture<'static, Result<proto::GetCommitDataResponse>>),
479    Idle,
480    Closed,
481}
482
483pub struct InitialGitGraphData {
484    fetch_task: Task<()>,
485    pub error: Option<SharedString>,
486    pub commit_data: Vec<Arc<InitialGraphCommitData>>,
487    pub commit_oid_to_index: HashMap<Oid, usize>,
488    subscribers: Vec<async_channel::Sender<Result<Vec<Arc<InitialGraphCommitData>>, SharedString>>>,
489}
490
491pub struct GraphDataResponse<'a> {
492    pub commits: &'a [Arc<InitialGraphCommitData>],
493    pub is_loading: bool,
494    pub error: Option<SharedString>,
495}
496
497pub struct Repository {
498    this: WeakEntity<Self>,
499    snapshot: RepositorySnapshot,
500    commit_message_buffer: Option<Entity<Buffer>>,
501    git_store: WeakEntity<GitStore>,
502    // For a local repository, holds paths that have had worktree events since the last status scan completed,
503    // and that should be examined during the next status scan.
504    paths_needing_status_update: Vec<Vec<RepoPath>>,
505    job_sender: mpsc::UnboundedSender<GitJob>,
506    _worker_task: Task<()>,
507    active_jobs: HashMap<JobId, JobInfo>,
508    job_debug_queue: job_debug_queue::GitJobDebugQueue,
509    pending_ops: SumTree<PendingOps>,
510    job_id: JobId,
511    askpass_delegates: Arc<Mutex<HashMap<u64, AskPassDelegate>>>,
512    latest_askpass_id: u64,
513    repository_state: Shared<Task<Result<RepositoryState, String>>>,
514    initial_graph_data: HashMap<(LogSource, LogOrder), InitialGitGraphData>,
515    commit_data_handler: CommitDataHandlerState,
516    commit_data: HashMap<Oid, CommitDataState>,
517}
518
519impl std::ops::Deref for Repository {
520    type Target = RepositorySnapshot;
521
522    fn deref(&self) -> &Self::Target {
523        &self.snapshot
524    }
525}
526
527#[derive(Clone)]
528pub struct LocalRepositoryState {
529    pub fs: Arc<dyn Fs>,
530    pub backend: Arc<dyn GitRepository>,
531    pub environment: Arc<HashMap<String, String>>,
532}
533
534impl LocalRepositoryState {
535    async fn new(
536        work_directory_abs_path: Arc<Path>,
537        dot_git_abs_path: Arc<Path>,
538        project_environment: WeakEntity<ProjectEnvironment>,
539        fs: Arc<dyn Fs>,
540        is_trusted: bool,
541        cx: &mut AsyncApp,
542    ) -> anyhow::Result<Self> {
543        let environment = project_environment
544                .update(cx, |project_environment, cx| {
545                    project_environment.local_directory_environment(&Shell::System, work_directory_abs_path.clone(), cx)
546                })?
547                .await
548                .unwrap_or_else(|| {
549                    log::error!("failed to get working directory environment for repository {work_directory_abs_path:?}");
550                    HashMap::default()
551                });
552        let search_paths = environment.get("PATH").map(|val| val.to_owned());
553        let backend = cx
554            .background_spawn({
555                let fs = fs.clone();
556                async move {
557                    let system_git_binary_path = search_paths
558                        .and_then(|search_paths| {
559                            which::which_in("git", Some(search_paths), &work_directory_abs_path)
560                                .ok()
561                        })
562                        .or_else(|| which::which("git").ok());
563                    fs.open_repo(&dot_git_abs_path, system_git_binary_path.as_deref())
564                        .with_context(|| format!("opening repository at {dot_git_abs_path:?}"))
565                }
566            })
567            .await?;
568        backend.set_trusted(is_trusted);
569        Ok(LocalRepositoryState {
570            backend,
571            environment: Arc::new(environment),
572            fs,
573        })
574    }
575}
576
577#[derive(Clone)]
578pub struct RemoteRepositoryState {
579    pub project_id: ProjectId,
580    pub client: AnyProtoClient,
581}
582
583#[derive(Clone)]
584pub enum RepositoryState {
585    Local(LocalRepositoryState),
586    Remote(RemoteRepositoryState),
587}
588
589#[derive(Clone, Debug, PartialEq, Eq)]
590pub enum GitGraphEvent {
591    CountUpdated(usize),
592    FullyLoaded,
593    LoadingError,
594}
595
596#[derive(Clone, Debug, PartialEq, Eq)]
597pub enum RepositoryEvent {
598    StatusesChanged,
599    HeadChanged,
600    BranchListChanged,
601    StashEntriesChanged,
602    GitWorktreeListChanged,
603    PendingOpsChanged { pending_ops: SumTree<PendingOps> },
604    GraphEvent((LogSource, LogOrder), GitGraphEvent),
605    GitDirectoryChanged,
606}
607
608#[derive(Clone, Debug)]
609pub struct JobsUpdated;
610
611#[derive(Debug)]
612pub enum GitStoreEvent {
613    ActiveRepositoryChanged(Option<RepositoryId>),
614    /// Bool is true when the repository that's updated is the active repository
615    RepositoryUpdated(RepositoryId, RepositoryEvent, bool),
616    RepositoryAdded,
617    RepositoryRemoved(RepositoryId),
618    IndexWriteError(anyhow::Error),
619    JobsUpdated,
620    ConflictsUpdated,
621    GlobalConfigurationUpdated,
622}
623
624impl EventEmitter<RepositoryEvent> for Repository {}
625impl EventEmitter<JobsUpdated> for Repository {}
626impl EventEmitter<GitStoreEvent> for GitStore {}
627
628pub struct GitJob {
629    id: JobId,
630    job: Box<dyn FnOnce(RepositoryState, &mut AsyncApp) -> Task<()>>,
631    key: Option<GitJobKey>,
632}
633
634#[derive(PartialEq, Eq)]
635enum GitJobKey {
636    WriteIndex(Vec<RepoPath>),
637    ReloadBufferDiffBases,
638    RefreshStatuses,
639    ReloadGitState,
640}
641
642impl GitStore {
643    pub fn local(
644        worktree_store: &Entity<WorktreeStore>,
645        buffer_store: Entity<BufferStore>,
646        environment: Entity<ProjectEnvironment>,
647        fs: Arc<dyn Fs>,
648        cx: &mut Context<Self>,
649    ) -> Self {
650        let _fs_watches = if fs.is_fake() {
651            Box::new([])
652        } else {
653            [
654                config_dir().join("git/config"),
655                home_dir().join(".gitconfig"),
656            ]
657            .into_iter()
658            .map(|path| {
659                let fs = fs.clone();
660
661                cx.spawn(async move |this, cx| {
662                    let watcher = fs.watch(&path, Duration::from_millis(100));
663                    let (mut watcher, _) = watcher.await;
664                    while let Some(_) = watcher.next().await {
665                        let Ok(_) = this.update(cx, |this, cx| {
666                            let GitStoreState::Local {
667                                project_environment,
668                                fs,
669                                ..
670                            } = &this.state
671                            else {
672                                return;
673                            };
674                            let project_environment = project_environment.downgrade();
675                            let fs = fs.clone();
676                            let repositories_to_respawn = this
677                                .repositories
678                                .iter()
679                                .filter_map(|(repository_id, repo)| {
680                                    repo.read(cx)
681                                        .job_sender
682                                        .is_closed()
683                                        .then_some((*repository_id, repo.clone()))
684                                })
685                                .collect::<Vec<_>>();
686                            for (repository_id, repo) in repositories_to_respawn {
687                                let is_trusted = this.repository_is_trusted(repository_id, cx);
688                                repo.update(cx, |repo, cx| {
689                                    repo.respawn_local_worker(
690                                        project_environment.clone(),
691                                        fs.clone(),
692                                        is_trusted,
693                                        cx,
694                                    );
695                                    repo.schedule_scan(None, cx);
696                                })
697                            }
698                            cx.emit(GitStoreEvent::GlobalConfigurationUpdated);
699                        }) else {
700                            return;
701                        };
702                    }
703                })
704            })
705            .collect::<Vec<_>>()
706            .into_boxed_slice()
707        };
708
709        Self::new(
710            worktree_store.clone(),
711            buffer_store,
712            GitStoreState::Local {
713                next_repository_id: Arc::new(AtomicU64::new(1)),
714                downstream: None,
715                project_environment: environment,
716                _fs_watches,
717                fs,
718            },
719            cx,
720        )
721    }
722
723    pub fn remote(
724        worktree_store: &Entity<WorktreeStore>,
725        buffer_store: Entity<BufferStore>,
726        upstream_client: AnyProtoClient,
727        project_id: u64,
728        cx: &mut Context<Self>,
729    ) -> Self {
730        Self::new(
731            worktree_store.clone(),
732            buffer_store,
733            GitStoreState::Remote {
734                upstream_client,
735                upstream_project_id: project_id,
736                downstream: None,
737            },
738            cx,
739        )
740    }
741
742    fn new(
743        worktree_store: Entity<WorktreeStore>,
744        buffer_store: Entity<BufferStore>,
745        state: GitStoreState,
746        cx: &mut Context<Self>,
747    ) -> Self {
748        let mut _subscriptions = vec![
749            cx.subscribe(&worktree_store, Self::on_worktree_store_event),
750            cx.subscribe(&buffer_store, Self::on_buffer_store_event),
751        ];
752
753        if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
754            _subscriptions.push(cx.subscribe(&trusted_worktrees, Self::on_trusted_worktrees_event));
755        }
756
757        GitStore {
758            state,
759            buffer_store,
760            worktree_store,
761            repositories: HashMap::default(),
762            worktree_ids: HashMap::default(),
763            active_repo_id: None,
764            _subscriptions,
765            loading_diffs: HashMap::default(),
766            shared_diffs: HashMap::default(),
767            diffs: HashMap::default(),
768            buffer_ids_by_index_text_buffer_id: HashMap::default(),
769        }
770    }
771
772    pub fn init(client: &AnyProtoClient) {
773        client.add_entity_request_handler(Self::handle_get_remotes);
774        client.add_entity_request_handler(Self::handle_get_branches);
775        client.add_entity_request_handler(Self::handle_get_default_branch);
776        client.add_entity_request_handler(Self::handle_change_branch);
777        client.add_entity_request_handler(Self::handle_create_branch);
778        client.add_entity_request_handler(Self::handle_rename_branch);
779        client.add_entity_request_handler(Self::handle_create_remote);
780        client.add_entity_request_handler(Self::handle_remove_remote);
781        client.add_entity_request_handler(Self::handle_delete_branch);
782        client.add_entity_request_handler(Self::handle_git_init);
783        client.add_entity_request_handler(Self::handle_push);
784        client.add_entity_request_handler(Self::handle_pull);
785        client.add_entity_request_handler(Self::handle_fetch);
786        client.add_entity_request_handler(Self::handle_stage);
787        client.add_entity_request_handler(Self::handle_unstage);
788        client.add_entity_request_handler(Self::handle_stash);
789        client.add_entity_request_handler(Self::handle_stash_pop);
790        client.add_entity_request_handler(Self::handle_stash_apply);
791        client.add_entity_request_handler(Self::handle_stash_drop);
792        client.add_entity_request_handler(Self::handle_commit);
793        client.add_entity_request_handler(Self::handle_run_hook);
794        client.add_entity_request_handler(Self::handle_reset);
795        client.add_entity_request_handler(Self::handle_show);
796        client.add_entity_request_handler(Self::handle_create_checkpoint);
797        client.add_entity_request_handler(Self::handle_create_archive_checkpoint);
798        client.add_entity_request_handler(Self::handle_restore_checkpoint);
799        client.add_entity_request_handler(Self::handle_restore_archive_checkpoint);
800        client.add_entity_request_handler(Self::handle_compare_checkpoints);
801        client.add_entity_request_handler(Self::handle_diff_checkpoints);
802        client.add_entity_request_handler(Self::handle_load_commit_diff);
803        client.add_entity_request_handler(Self::handle_checkout_files);
804        client.add_entity_request_handler(Self::handle_add_path_to_gitignore);
805        client.add_entity_request_handler(Self::handle_add_path_to_git_info_exclude);
806        client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
807        client.add_entity_request_handler(Self::handle_set_index_text);
808        client.add_entity_request_handler(Self::handle_askpass);
809        client.add_entity_request_handler(Self::handle_check_for_pushed_commits);
810        client.add_entity_request_handler(Self::handle_git_diff);
811        client.add_entity_request_handler(Self::handle_tree_diff);
812        client.add_entity_request_handler(Self::handle_get_blob_content);
813        client.add_entity_request_handler(Self::handle_load_commit_template);
814        client.add_entity_request_handler(Self::handle_open_unstaged_diff);
815        client.add_entity_request_handler(Self::handle_open_uncommitted_diff);
816        client.add_entity_message_handler(Self::handle_update_diff_bases);
817        client.add_entity_request_handler(Self::handle_get_permalink_to_line);
818        client.add_entity_request_handler(Self::handle_blame_buffer);
819        client.add_entity_message_handler(Self::handle_update_repository);
820        client.add_entity_message_handler(Self::handle_remove_repository);
821        client.add_entity_request_handler(Self::handle_git_clone);
822        client.add_entity_request_handler(Self::handle_get_worktrees);
823        client.add_entity_request_handler(Self::handle_create_worktree);
824        client.add_entity_request_handler(Self::handle_remove_worktree);
825        client.add_entity_request_handler(Self::handle_rename_worktree);
826        client.add_entity_request_handler(Self::handle_worktree_created_at);
827        client.add_entity_request_handler(Self::handle_get_head_sha);
828        client.add_entity_request_handler(Self::handle_edit_ref);
829        client.add_entity_request_handler(Self::handle_repair_worktrees);
830        client.add_entity_request_handler(Self::handle_get_commit_data);
831        client.add_entity_stream_request_handler(Self::handle_get_initial_graph_data);
832        client.add_entity_stream_request_handler(Self::handle_search_commits);
833    }
834
835    pub fn is_local(&self) -> bool {
836        matches!(self.state, GitStoreState::Local { .. })
837    }
838
839    fn set_active_repo_id(&mut self, repo_id: RepositoryId, cx: &mut Context<Self>) {
840        if self.active_repo_id != Some(repo_id) {
841            self.active_repo_id = Some(repo_id);
842            cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(repo_id)));
843        }
844    }
845
846    pub fn set_active_repo_for_path(&mut self, project_path: &ProjectPath, cx: &mut Context<Self>) {
847        if let Some((repo, _)) = self.repository_and_path_for_project_path(project_path, cx) {
848            self.set_active_repo_id(repo.read(cx).id, cx);
849        }
850    }
851
852    pub fn set_active_repo_for_worktree(
853        &mut self,
854        worktree_id: WorktreeId,
855        cx: &mut Context<Self>,
856    ) {
857        let Some(worktree) = self
858            .worktree_store
859            .read(cx)
860            .worktree_for_id(worktree_id, cx)
861        else {
862            return;
863        };
864        let worktree_abs_path = worktree.read(cx).abs_path();
865        let Some(repo_id) = self
866            .repositories
867            .values()
868            .filter(|repo| {
869                let repo_path = &repo.read(cx).work_directory_abs_path;
870                // The folder opened in Zed isn't necessarily the repo root; it may be
871                // a subdirectory of it, e.g. opening `~/code/myrepo/backend` when the
872                // repo lives at `~/code/myrepo`. So match any repo whose work directory
873                // contains the folder. Nested repos can produce multiple matches, e.g.
874                // opening `~/code/myrepo/vendor/lib` where `vendor/lib` is a submodule
875                // matches both `myrepo` and the submodule; `max_by_key` then picks the
876                // innermost match (the submodule), which the folder actually belongs to.
877                worktree_abs_path.starts_with(repo_path.as_ref())
878            })
879            .max_by_key(|repo| repo.read(cx).work_directory_abs_path.as_os_str().len())
880            .map(|repo| repo.read(cx).id)
881        else {
882            return;
883        };
884
885        self.set_active_repo_id(repo_id, cx);
886    }
887
888    pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
889        match &mut self.state {
890            GitStoreState::Remote {
891                downstream: downstream_client,
892                ..
893            } => {
894                for repo in self.repositories.values() {
895                    let update = repo.read(cx).snapshot.initial_update(project_id);
896                    for update in split_repository_update(update) {
897                        client.send(update).log_err();
898                    }
899                }
900                *downstream_client = Some((client, ProjectId(project_id)));
901            }
902            GitStoreState::Local {
903                downstream: downstream_client,
904                ..
905            } => {
906                let mut snapshots = HashMap::default();
907                let (updates_tx, mut updates_rx) = mpsc::unbounded();
908                for repo in self.repositories.values() {
909                    updates_tx
910                        .unbounded_send(DownstreamUpdate::UpdateRepository(
911                            repo.read(cx).snapshot.clone(),
912                        ))
913                        .ok();
914                }
915                *downstream_client = Some(LocalDownstreamState {
916                    client: client.clone(),
917                    project_id: ProjectId(project_id),
918                    updates_tx,
919                    _task: cx.spawn(async move |this, cx| {
920                        cx.background_spawn(async move {
921                            while let Some(update) = updates_rx.next().await {
922                                match update {
923                                    DownstreamUpdate::UpdateRepository(snapshot) => {
924                                        if let Some(old_snapshot) = snapshots.get_mut(&snapshot.id)
925                                        {
926                                            let update =
927                                                snapshot.build_update(old_snapshot, project_id);
928                                            *old_snapshot = snapshot;
929                                            for update in split_repository_update(update) {
930                                                client.send(update)?;
931                                            }
932                                        } else {
933                                            let update = snapshot.initial_update(project_id);
934                                            for update in split_repository_update(update) {
935                                                client.send(update)?;
936                                            }
937                                            snapshots.insert(snapshot.id, snapshot);
938                                        }
939                                    }
940                                    DownstreamUpdate::RemoveRepository(id) => {
941                                        client.send(proto::RemoveRepository {
942                                            project_id,
943                                            id: id.to_proto(),
944                                        })?;
945                                    }
946                                }
947                            }
948                            anyhow::Ok(())
949                        })
950                        .await
951                        .ok();
952                        this.update(cx, |this, _| {
953                            if let GitStoreState::Local {
954                                downstream: downstream_client,
955                                ..
956                            } = &mut this.state
957                            {
958                                downstream_client.take();
959                            } else {
960                                unreachable!("unshared called on remote store");
961                            }
962                        })
963                    }),
964                });
965            }
966        }
967    }
968
969    pub fn unshared(&mut self, _cx: &mut Context<Self>) {
970        match &mut self.state {
971            GitStoreState::Local {
972                downstream: downstream_client,
973                ..
974            } => {
975                downstream_client.take();
976            }
977            GitStoreState::Remote {
978                downstream: downstream_client,
979                ..
980            } => {
981                downstream_client.take();
982            }
983        }
984        self.shared_diffs.clear();
985    }
986
987    pub(crate) fn forget_shared_diffs_for(&mut self, peer_id: &proto::PeerId) {
988        self.shared_diffs.remove(peer_id);
989    }
990
991    pub fn active_repository(&self) -> Option<Entity<Repository>> {
992        self.active_repo_id
993            .as_ref()
994            .map(|id| self.repositories[id].clone())
995    }
996
997    fn file_is_symlink(file: &File, cx: &App) -> bool {
998        file.worktree
999            .read(cx)
1000            .entry_for_path(&file.path)
1001            .is_some_and(|entry| entry.canonical_path.is_some())
1002    }
1003
1004    fn buffer_is_symlink(buffer: &Entity<Buffer>, cx: &App) -> bool {
1005        File::from_dyn(buffer.read(cx).file()).is_some_and(|file| Self::file_is_symlink(file, cx))
1006    }
1007
1008    pub fn open_unstaged_diff(
1009        &mut self,
1010        buffer: Entity<Buffer>,
1011        cx: &mut Context<Self>,
1012    ) -> Task<Result<Entity<BufferDiff>>> {
1013        let buffer_id = buffer.read(cx).remote_id();
1014        if let Some(diff_state) = self.diffs.get(&buffer_id)
1015            && let Some(unstaged_diff) = diff_state
1016                .read(cx)
1017                .unstaged_diff
1018                .as_ref()
1019                .and_then(|weak| weak.upgrade())
1020        {
1021            // If this unstaged diff was first opened as the uncommitted diff's
1022            // secondary, its index text wasn't highlighted. Enable it now and
1023            // recalc so the language gets applied to the deleted (base) side.
1024            diff_state.update(cx, |diff_state, cx| {
1025                if !diff_state.index_text_buffer_language_enabled {
1026                    diff_state.index_text_buffer_language_enabled = true;
1027                    let buffer_snapshot = buffer.read(cx).text_snapshot();
1028                    diff_state.recalculate_diffs(buffer_snapshot, cx);
1029                }
1030            });
1031            if let Some(task) =
1032                diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
1033            {
1034                return cx.background_executor().spawn(async move {
1035                    task.await;
1036                    Ok(unstaged_diff)
1037                });
1038            }
1039            return Task::ready(Ok(unstaged_diff));
1040        }
1041
1042        let Some((repo, repo_path)) =
1043            self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
1044        else {
1045            return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
1046        };
1047
1048        let is_symlink = Self::buffer_is_symlink(&buffer, cx);
1049        let task = self
1050            .loading_diffs
1051            .entry((buffer_id, DiffKind::Unstaged))
1052            .or_insert_with(|| {
1053                let staged_text = if is_symlink {
1054                    Task::ready(Ok(None))
1055                } else {
1056                    repo.update(cx, |repo, cx| {
1057                        repo.load_staged_text(buffer_id, repo_path, cx)
1058                    })
1059                };
1060                cx.spawn(async move |this, cx| {
1061                    Self::open_diff_internal(
1062                        this,
1063                        DiffKind::Unstaged,
1064                        staged_text.await.map(DiffBasesChange::SetIndex),
1065                        buffer,
1066                        cx,
1067                    )
1068                    .await
1069                    .map_err(Arc::new)
1070                })
1071                .shared()
1072            })
1073            .clone();
1074
1075        cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
1076    }
1077
1078    /// Opens the staged (HEAD-vs-index) diff for the given buffer, along with
1079    /// the index text buffer that is the diff's main buffer.
1080    pub fn open_staged_diff(
1081        &mut self,
1082        buffer: Entity<Buffer>,
1083        cx: &mut Context<Self>,
1084    ) -> Task<Result<(Entity<BufferDiff>, Entity<Buffer>)>> {
1085        let buffer_id = buffer.read(cx).remote_id();
1086
1087        if let Some(diff_state) = self.diffs.get(&buffer_id)
1088            && let Some(staged_diff) = diff_state.read(cx).staged_diff_and_index_text_buffer()
1089        {
1090            if let Some(task) =
1091                diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
1092            {
1093                return cx.background_executor().spawn(async move {
1094                    task.await;
1095                    Ok(staged_diff)
1096                });
1097            }
1098            return Task::ready(Ok(staged_diff));
1099        }
1100
1101        let Some((repo, repo_path)) =
1102            self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
1103        else {
1104            return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
1105        };
1106
1107        let task = self
1108            .loading_diffs
1109            .entry((buffer_id, DiffKind::Staged))
1110            .or_insert_with(|| {
1111                let changes = repo.update(cx, |repo, cx| {
1112                    repo.load_committed_text(buffer_id, repo_path, cx)
1113                });
1114
1115                cx.spawn(async move |this, cx| {
1116                    Self::open_diff_internal(this, DiffKind::Staged, changes.await, buffer, cx)
1117                        .await
1118                        .map_err(Arc::new)
1119                })
1120                .shared()
1121            })
1122            .clone();
1123
1124        cx.spawn(async move |this, cx| {
1125            let diff = task.await.map_err(|e| anyhow!("{e}"))?;
1126            this.update(cx, |this, cx| {
1127                let index_text_buffer = this
1128                    .diffs
1129                    .get(&buffer_id)
1130                    .and_then(|diff_state| {
1131                        let (_, index_text_buffer) = diff_state.read(cx).staged_diff.as_ref()?;
1132                        Some(index_text_buffer.clone())
1133                    })
1134                    .context("index text buffer missing after opening staged diff")?;
1135                Ok((diff, index_text_buffer))
1136            })?
1137        })
1138    }
1139
1140    /// Stages the worktree changes covered by `worktree_ranges`, acting on the
1141    /// given unstaged (index-vs-worktree) diff. Used by both the unstaged-changes
1142    /// view and the uncommitted (gutter) controls: "stage" means the same index
1143    /// change regardless of which view it was invoked from, so callers holding an
1144    /// uncommitted diff pass its unstaged secondary.
1145    ///
1146    /// Decomposes the worktree region into the unstaged hunks it covers, so no
1147    /// worktree->index projection is needed. Optimistically suppresses the staged
1148    /// hunks from the unstaged diff and, if the uncommitted diff happens to be
1149    /// open, marks the corresponding uncommitted hunks as staging.
1150    pub fn stage_hunks(
1151        &mut self,
1152        buffer: Entity<Buffer>,
1153        unstaged_diff: Entity<BufferDiff>,
1154        worktree_ranges: Vec<Range<Anchor>>,
1155        cx: &mut Context<Self>,
1156    ) -> Result<()> {
1157        if worktree_ranges.is_empty() {
1158            return Ok(());
1159        }
1160        let buffer_snapshot = buffer.read(cx).snapshot();
1161        let buffer_id = buffer_snapshot.remote_id();
1162        let file_exists = buffer_snapshot
1163            .file()
1164            .is_some_and(|file| file.disk_state().exists());
1165
1166        let unstaged_snapshot = unstaged_diff.read(cx).snapshot(cx);
1167
1168        // Decompose: the unstaged hunks the worktree region covers carry the
1169        // index range directly. Sorting by buffer offset also sorts by index
1170        // offset, since the hunks are non-overlapping. We read the raw hunks
1171        // (ignoring optimistic suppression) so that re-staging a hunk that was
1172        // optimistically staged and then unstaged still finds it. The footprints
1173        // let the patch drop earlier edits the region covers even where the raw
1174        // diff is empty (re-staging a hunk that is staged on disk but
1175        // optimistically unstaged).
1176        let mut unstaged_hunks = Vec::new();
1177        let mut index_footprints = Vec::new();
1178        for range in &worktree_ranges {
1179            unstaged_hunks.extend(
1180                unstaged_snapshot.raw_hunks_intersecting_range(range.clone(), &buffer_snapshot),
1181            );
1182            index_footprints.push(
1183                unstaged_snapshot.base_text_range_for_buffer_range(range.clone(), &buffer_snapshot),
1184            );
1185        }
1186        unstaged_hunks.sort_by_key(|hunk| hunk.buffer_range.start.to_offset(&buffer_snapshot));
1187        unstaged_hunks.dedup_by(|a, b| a.buffer_range.start == b.buffer_range.start);
1188
1189        // The uncommitted hunks the region covers get the optimistic
1190        // "staging" secondary status (free cross-view update).
1191        let uncommitted_diff = self.get_uncommitted_diff(buffer_id, cx);
1192        let mut uncommitted_hunks = Vec::new();
1193        if let Some(uncommitted_diff) = &uncommitted_diff {
1194            let uncommitted_snapshot = uncommitted_diff.read(cx).snapshot(cx);
1195            for range in &worktree_ranges {
1196                uncommitted_hunks.extend(
1197                    uncommitted_snapshot
1198                        .hunks_intersecting_range(range.clone(), &buffer_snapshot)
1199                        .filter(|hunk| {
1200                            hunk.secondary_status != DiffHunkSecondaryStatus::NoSecondaryHunk
1201                        }),
1202                );
1203            }
1204            uncommitted_hunks
1205                .sort_by_key(|hunk| hunk.buffer_range.start.to_offset(&buffer_snapshot));
1206            uncommitted_hunks.dedup_by(|a, b| a.buffer_range.start == b.buffer_range.start);
1207        }
1208
1209        let index_edits = if !file_exists {
1210            // The worktree file is gone: staging removes it from the index.
1211            None
1212        } else {
1213            Some(
1214                unstaged_hunks
1215                    .iter()
1216                    .map(|hunk| {
1217                        let worktree_range = hunk.buffer_range.to_offset(&buffer_snapshot);
1218                        let replacement: Arc<str> = Arc::from(
1219                            buffer_snapshot
1220                                .text_for_range(worktree_range)
1221                                .collect::<String>(),
1222                        );
1223                        (hunk.diff_base_byte_range.clone(), replacement)
1224                    })
1225                    .collect::<Vec<_>>(),
1226            )
1227        };
1228
1229        let version = buffer_snapshot.version().clone();
1230        let unstaged_pending = pending_hunks(&unstaged_hunks, &version, PendingSense::Suppress);
1231        let uncommitted_pending = pending_hunks(
1232            &uncommitted_hunks,
1233            &version,
1234            PendingSense::SetSecondaryStatus { stage: true },
1235        );
1236        drop(unstaged_snapshot);
1237
1238        let diff_state = self
1239            .diffs
1240            .get(&buffer_id)
1241            .cloned()
1242            .context("failed to find git state for buffer")?;
1243        diff_state.update(cx, |diff_state, _| {
1244            diff_state.remove_overlapping_pending_index_edits(&index_footprints);
1245            diff_state.insert_pending_index_edits(index_edits);
1246        });
1247
1248        if let Some(uncommitted_diff) = uncommitted_diff {
1249            uncommitted_diff.update(cx, |diff, cx| {
1250                diff.set_pending_hunks(&uncommitted_pending, &buffer_snapshot, cx);
1251            });
1252        }
1253        unstaged_diff.update(cx, |diff, cx| {
1254            diff.set_pending_hunks(&unstaged_pending, &buffer_snapshot, cx);
1255        });
1256
1257        self.write_optimistic_index(buffer_id, cx);
1258        Ok(())
1259    }
1260
1261    /// Unstages the worktree changes covered by `worktree_ranges`, acting on the
1262    /// given uncommitted (HEAD-vs-worktree) diff, invoked from the uncommitted
1263    /// (gutter) controls. Uses the worktree->index projection (the hard part)
1264    /// because the acted-on hunks are HEAD-vs-worktree.
1265    pub fn unstage_uncommitted_hunks(
1266        &mut self,
1267        buffer: Entity<Buffer>,
1268        uncommitted_diff: Entity<BufferDiff>,
1269        worktree_ranges: Vec<Range<Anchor>>,
1270        cx: &mut Context<Self>,
1271    ) -> Result<()> {
1272        if worktree_ranges.is_empty() {
1273            return Ok(());
1274        }
1275        let buffer_snapshot = buffer.read(cx).snapshot();
1276        let buffer_id = buffer_snapshot.remote_id();
1277        let file_exists = buffer_snapshot
1278            .file()
1279            .is_some_and(|file| file.disk_state().exists());
1280
1281        let uncommitted_snapshot = uncommitted_diff.read(cx).snapshot(cx);
1282        let unstaged_snapshot = uncommitted_snapshot
1283            .secondary_diff()
1284            .context("diff has no unstaged secondary")?;
1285
1286        let mut hunks = Vec::new();
1287        for range in &worktree_ranges {
1288            hunks.extend(
1289                uncommitted_snapshot.hunks_intersecting_range(range.clone(), &buffer_snapshot),
1290            );
1291        }
1292        hunks.sort_by_key(|hunk| hunk.buffer_range.start.to_offset(&buffer_snapshot));
1293        hunks.dedup_by(|a, b| a.buffer_range.start == b.buffer_range.start);
1294
1295        let (index_edits, pending) = uncommitted_snapshot.compute_uncommitted_index_edits(
1296            unstaged_snapshot,
1297            false,
1298            &hunks,
1299            &buffer_snapshot,
1300            file_exists,
1301        );
1302        drop(uncommitted_snapshot);
1303
1304        let diff_state = self
1305            .diffs
1306            .get(&buffer_id)
1307            .cloned()
1308            .context("failed to find git state for buffer")?;
1309        diff_state.update(cx, |diff_state, _| {
1310            diff_state.insert_pending_index_edits(index_edits)
1311        });
1312
1313        uncommitted_diff.update(cx, |diff, cx| {
1314            diff.set_pending_hunks(&pending, &buffer_snapshot, cx);
1315        });
1316
1317        self.write_optimistic_index(buffer_id, cx);
1318        Ok(())
1319    }
1320
1321    /// Unstages staged (HEAD-vs-index) hunks covered by `index_ranges` (in the
1322    /// index text buffer's coordinates), acting on the given staged diff,
1323    /// invoked from the staged-changes view. The acted-on hunks already carry
1324    /// an index range, so no projection is needed; optimistically suppresses
1325    /// them from the staged diff.
1326    pub fn unstage_staged_hunks(
1327        &mut self,
1328        staged_diff: Entity<BufferDiff>,
1329        index_ranges: Vec<Range<Anchor>>,
1330        cx: &mut Context<Self>,
1331    ) -> Result<()> {
1332        if index_ranges.is_empty() {
1333            return Ok(());
1334        }
1335        let index_buffer_id = staged_diff.read(cx).buffer_id;
1336        let buffer_id = *self
1337            .buffer_ids_by_index_text_buffer_id
1338            .get(&index_buffer_id)
1339            .context("failed to find git state for index text buffer")?;
1340        let diff_state = self
1341            .diffs
1342            .get(&buffer_id)
1343            .cloned()
1344            .context("failed to find git state for buffer")?;
1345        let index_buffer = diff_state
1346            .read(cx)
1347            .index_text_buffer()
1348            .context("index text is not loaded")?;
1349        let index_snapshot = index_buffer.read(cx).text_snapshot();
1350        let staged_snapshot = staged_diff.read(cx).snapshot(cx);
1351
1352        let mut hunks = Vec::new();
1353        for range in &index_ranges {
1354            hunks.extend(staged_snapshot.hunks_intersecting_range(range.clone(), &index_snapshot));
1355        }
1356        hunks.sort_by_key(|hunk| hunk.buffer_range.start.to_offset(&index_snapshot));
1357        hunks.dedup_by(|a, b| a.buffer_range.start == b.buffer_range.start);
1358
1359        let index_edits = staged_diff
1360            .read(cx)
1361            .unstage_staged_hunks(&hunks, &index_snapshot);
1362        let version = index_snapshot.version().clone();
1363        let pending = pending_hunks(&hunks, &version, PendingSense::Suppress);
1364        drop(staged_snapshot);
1365
1366        diff_state.update(cx, |diff_state, _| {
1367            diff_state.insert_pending_index_edits(index_edits)
1368        });
1369        staged_diff.update(cx, |diff, cx| {
1370            diff.set_pending_hunks(&pending, &index_snapshot, cx);
1371        });
1372
1373        self.write_optimistic_index(buffer_id, cx);
1374        Ok(())
1375    }
1376
1377    /// Derives the desired index text from the buffer's optimistic patch and
1378    /// schedules the write.
1379    fn write_optimistic_index(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
1380        let Some(diff_state) = self.diffs.get(&buffer_id) else {
1381            return;
1382        };
1383        let new_index_text = diff_state
1384            .read(cx)
1385            .pending_index_text(cx)
1386            .map(|rope| rope.to_string());
1387        self.write_index_text_for_buffer_id(buffer_id, new_index_text, cx);
1388    }
1389
1390    pub fn open_diff_since(
1391        &mut self,
1392        oid: Option<git::Oid>,
1393        buffer: Entity<Buffer>,
1394        repo: Entity<Repository>,
1395        cx: &mut Context<Self>,
1396    ) -> Task<Result<Entity<BufferDiff>>> {
1397        let buffer_id = buffer.read(cx).remote_id();
1398
1399        if let Some(diff_state) = self.diffs.get(&buffer_id)
1400            && let Some(oid_diff) = diff_state.read(cx).oid_diff(oid)
1401        {
1402            if let Some(task) =
1403                diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
1404            {
1405                return cx.background_executor().spawn(async move {
1406                    task.await;
1407                    Ok(oid_diff)
1408                });
1409            }
1410            return Task::ready(Ok(oid_diff));
1411        }
1412
1413        let diff_kind = DiffKind::SinceOid(oid);
1414        if let Some(task) = self.loading_diffs.get(&(buffer_id, diff_kind)) {
1415            let task = task.clone();
1416            return cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) });
1417        }
1418
1419        let task = cx
1420            .spawn(async move |this, cx| {
1421                let result: Result<Entity<BufferDiff>> = async {
1422                    let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
1423                    let language_registry =
1424                        buffer.update(cx, |buffer, _| buffer.language_registry());
1425                    let content: Option<Arc<str>> = match oid {
1426                        None => None,
1427                        Some(oid) => Some({
1428                            let mut content = repo
1429                                .update(cx, |repo, cx| repo.load_blob_content(oid, cx))
1430                                .await?;
1431                            text::LineEnding::normalize(&mut content);
1432                            content.into()
1433                        }),
1434                    };
1435                    let buffer_diff = cx.new(|cx| {
1436                        BufferDiff::new(
1437                            &buffer_snapshot,
1438                            buffer_snapshot.language().cloned(),
1439                            language_registry,
1440                            cx,
1441                        )
1442                    });
1443
1444                    buffer_diff
1445                        .update(cx, |buffer_diff, cx| {
1446                            buffer_diff.set_base_text(content.clone(), buffer_snapshot.text, cx)
1447                        })
1448                        .await;
1449                    let unstaged_diff = this
1450                        .update(cx, |this, cx| this.open_unstaged_diff(buffer.clone(), cx))?
1451                        .await?;
1452                    buffer_diff.update(cx, |buffer_diff, _| {
1453                        buffer_diff.set_secondary_diff(unstaged_diff);
1454                    });
1455
1456                    this.update(cx, |this, cx| {
1457                        this.loading_diffs.remove(&(buffer_id, diff_kind));
1458
1459                        let git_store = cx.weak_entity();
1460                        let diff_state = this
1461                            .diffs
1462                            .entry(buffer_id)
1463                            .or_insert_with(|| cx.new(|cx| BufferGitState::new(git_store, cx)));
1464
1465                        diff_state.update(cx, |state, _| {
1466                            if let Some(oid) = oid {
1467                                if let Some(content) = content {
1468                                    state.oid_texts.insert(oid, content);
1469                                }
1470                            }
1471                            state.oid_diffs.insert(oid, buffer_diff.downgrade());
1472                        });
1473                    })?;
1474
1475                    Ok(buffer_diff)
1476                }
1477                .await;
1478                result.map_err(Arc::new)
1479            })
1480            .shared();
1481
1482        self.loading_diffs
1483            .insert((buffer_id, diff_kind), task.clone());
1484        cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
1485    }
1486
1487    #[ztracing::instrument(skip_all)]
1488    pub fn open_uncommitted_diff(
1489        &mut self,
1490        buffer: Entity<Buffer>,
1491        cx: &mut Context<Self>,
1492    ) -> Task<Result<Entity<BufferDiff>>> {
1493        let buffer_id = buffer.read(cx).remote_id();
1494
1495        if let Some(diff_state) = self.diffs.get(&buffer_id)
1496            && let Some(uncommitted_diff) = diff_state
1497                .read(cx)
1498                .uncommitted_diff
1499                .as_ref()
1500                .and_then(|weak| weak.upgrade())
1501        {
1502            if let Some(task) =
1503                diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
1504            {
1505                return cx.background_executor().spawn(async move {
1506                    task.await;
1507                    Ok(uncommitted_diff)
1508                });
1509            }
1510            return Task::ready(Ok(uncommitted_diff));
1511        }
1512
1513        let Some((repo, repo_path)) =
1514            self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
1515        else {
1516            return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
1517        };
1518
1519        let is_symlink = Self::buffer_is_symlink(&buffer, cx);
1520        let task = self
1521            .loading_diffs
1522            .entry((buffer_id, DiffKind::Uncommitted))
1523            .or_insert_with(|| {
1524                let changes = if is_symlink {
1525                    Task::ready(Ok(DiffBasesChange::SetBoth(None)))
1526                } else {
1527                    repo.update(cx, |repo, cx| {
1528                        repo.load_committed_text(buffer_id, repo_path, cx)
1529                    })
1530                };
1531
1532                // todo(lw): hot foreground spawn
1533                cx.spawn(async move |this, cx| {
1534                    Self::open_diff_internal(this, DiffKind::Uncommitted, changes.await, buffer, cx)
1535                        .await
1536                        .map_err(Arc::new)
1537                })
1538                .shared()
1539            })
1540            .clone();
1541
1542        cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
1543    }
1544
1545    #[ztracing::instrument(skip_all)]
1546    async fn open_diff_internal(
1547        this: WeakEntity<Self>,
1548        kind: DiffKind,
1549        texts: Result<DiffBasesChange>,
1550        buffer_entity: Entity<Buffer>,
1551        cx: &mut AsyncApp,
1552    ) -> Result<Entity<BufferDiff>> {
1553        let diff_bases_change = match texts {
1554            Err(e) => {
1555                this.update(cx, |this, cx| {
1556                    let buffer = buffer_entity.read(cx);
1557                    let buffer_id = buffer.remote_id();
1558                    this.loading_diffs.remove(&(buffer_id, kind));
1559                })?;
1560                return Err(e);
1561            }
1562            Ok(change) => change,
1563        };
1564
1565        this.update(cx, |this, cx| {
1566            let buffer = buffer_entity.read(cx);
1567            let buffer_id = buffer.remote_id();
1568            let language = buffer.language().cloned();
1569            let language_registry = buffer.language_registry();
1570            let index_text_file = buffer.file().map(|file| {
1571                Arc::new(IndexTextFile::new(file.as_ref(), cx)) as Arc<dyn language::File>
1572            });
1573            let text_snapshot = buffer.text_snapshot();
1574            this.loading_diffs.remove(&(buffer_id, kind));
1575
1576            let git_store = cx.weak_entity();
1577            let diff_state = this
1578                .diffs
1579                .entry(buffer_id)
1580                .or_insert_with(|| cx.new(|cx| BufferGitState::new(git_store, cx)));
1581
1582            let existing_unstaged_diff = diff_state.read(cx).unstaged_diff();
1583
1584            let mut staged_index_text_buffer = None;
1585            let diff = if kind == DiffKind::Unstaged
1586                && let Some(existing_unstaged_diff) = existing_unstaged_diff.clone()
1587            {
1588                existing_unstaged_diff
1589            } else {
1590                let diff = match kind {
1591                    DiffKind::Unstaged => {
1592                        let base_text_buffer = diff_state.update(cx, |diff_state, cx| {
1593                            diff_state.get_or_create_index_text_buffer(index_text_file.clone(), cx)
1594                        });
1595                        cx.new(|cx| {
1596                            BufferDiff::new_with_base_text_buffer(
1597                                &text_snapshot,
1598                                base_text_buffer,
1599                                cx,
1600                            )
1601                        })
1602                    }
1603                    DiffKind::Staged => {
1604                        let (index_text_buffer, base_text_buffer) =
1605                            diff_state.update(cx, |diff_state, cx| {
1606                                (
1607                                    diff_state.get_or_create_index_text_buffer(
1608                                        index_text_file.clone(),
1609                                        cx,
1610                                    ),
1611                                    diff_state.get_or_create_head_text_buffer(cx),
1612                                )
1613                            });
1614                        index_text_buffer.update(cx, |index_text_buffer, cx| {
1615                            if let Some(language_registry) = language_registry.clone() {
1616                                index_text_buffer.set_language_registry(language_registry);
1617                            }
1618                            index_text_buffer.set_language_async(language.clone(), cx);
1619                        });
1620                        let index_text_snapshot = index_text_buffer.read(cx).text_snapshot();
1621                        staged_index_text_buffer = Some(index_text_buffer);
1622                        cx.new(|cx| {
1623                            BufferDiff::new_with_base_text_buffer(
1624                                &index_text_snapshot,
1625                                base_text_buffer,
1626                                cx,
1627                            )
1628                        })
1629                    }
1630                    DiffKind::Uncommitted => {
1631                        let base_text_buffer = diff_state.update(cx, |diff_state, cx| {
1632                            diff_state.get_or_create_head_text_buffer(cx)
1633                        });
1634                        cx.new(|cx| {
1635                            BufferDiff::new_with_base_text_buffer(
1636                                &text_snapshot,
1637                                base_text_buffer,
1638                                cx,
1639                            )
1640                        })
1641                    }
1642                    DiffKind::SinceOid(_) => {
1643                        unreachable!("open_diff_internal is not used for OID diffs")
1644                    }
1645                };
1646                diff
1647            };
1648            let rx = diff_state.update(cx, |diff_state, cx| {
1649                diff_state.language = language;
1650                diff_state.language_registry = language_registry;
1651
1652                match kind {
1653                    DiffKind::Unstaged => {
1654                        diff_state.unstaged_diff = Some(diff.downgrade());
1655                        // The deleted (base) side of a standalone unstaged diff
1656                        // is the index, so highlight it. The recalc kicked off by
1657                        // `diff_bases_changed` below applies the language.
1658                        diff_state.index_text_buffer_language_enabled = true;
1659                    }
1660                    DiffKind::Staged => {
1661                        diff_state.index_text_buffer_language_enabled = true;
1662                        let index_text_buffer = staged_index_text_buffer
1663                            .take()
1664                            .context("index text buffer was not created for staged diff")?;
1665                        diff_state.staged_diff = Some((diff.downgrade(), index_text_buffer));
1666                    }
1667                    DiffKind::Uncommitted => {
1668                        let unstaged_diff = if let Some(diff) = existing_unstaged_diff {
1669                            diff
1670                        } else {
1671                            let base_text_buffer =
1672                                diff_state.get_or_create_index_text_buffer(index_text_file, cx);
1673                            let unstaged_diff = cx.new(|cx| {
1674                                BufferDiff::new_with_base_text_buffer(
1675                                    &text_snapshot,
1676                                    base_text_buffer,
1677                                    cx,
1678                                )
1679                            });
1680                            diff_state.unstaged_diff = Some(unstaged_diff.downgrade());
1681                            unstaged_diff
1682                        };
1683
1684                        diff.update(cx, |diff, _| diff.set_secondary_diff(unstaged_diff));
1685                        diff_state.uncommitted_diff = Some(diff.downgrade())
1686                    }
1687                    DiffKind::SinceOid(_) => {
1688                        unreachable!("open_diff_internal is not used for OID diffs")
1689                    }
1690                }
1691
1692                diff_state.diff_bases_changed(text_snapshot, Some(diff_bases_change), cx);
1693                let rx = diff_state.wait_for_recalculation();
1694
1695                anyhow::Ok(async move {
1696                    if let Some(rx) = rx {
1697                        rx.await;
1698                    }
1699                    Ok(diff)
1700                })
1701            });
1702            let diff_state = this.diffs.get(&buffer_id).cloned();
1703            if let Some(index_text_buffer) =
1704                diff_state.and_then(|diff_state| diff_state.read(cx).index_text_buffer())
1705            {
1706                this.buffer_ids_by_index_text_buffer_id
1707                    .insert(index_text_buffer.read(cx).remote_id(), buffer_id);
1708            }
1709            rx
1710        })??
1711        .await
1712    }
1713
1714    pub fn get_unstaged_diff(&self, buffer_id: BufferId, cx: &App) -> Option<Entity<BufferDiff>> {
1715        let diff_state = self.diffs.get(&buffer_id)?;
1716        diff_state.read(cx).unstaged_diff.as_ref()?.upgrade()
1717    }
1718
1719    pub fn get_staged_diff(&self, buffer_id: BufferId, cx: &App) -> Option<Entity<BufferDiff>> {
1720        let diff_state = self.diffs.get(&buffer_id)?;
1721        diff_state.read(cx).staged_diff()
1722    }
1723
1724    pub fn get_uncommitted_diff(
1725        &self,
1726        buffer_id: BufferId,
1727        cx: &App,
1728    ) -> Option<Entity<BufferDiff>> {
1729        let diff_state = self.diffs.get(&buffer_id)?;
1730        diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade()
1731    }
1732
1733    pub fn get_diff_since_oid(
1734        &self,
1735        buffer_id: BufferId,
1736        oid: Option<git::Oid>,
1737        cx: &App,
1738    ) -> Option<Entity<BufferDiff>> {
1739        let diff_state = self.diffs.get(&buffer_id)?;
1740        diff_state.read(cx).oid_diff(oid)
1741    }
1742
1743    /// Whether this buffer's index text is known to match its committed text
1744    /// without comparing contents, i.e. whether the texts share one allocation.
1745    /// In a downstream project, this can only be true if the upstream sent
1746    /// `Mode::IndexMatchesHead`.
1747    #[cfg(any(test, feature = "test-support"))]
1748    pub fn index_matches_head_for_buffer(&self, buffer_id: BufferId, cx: &App) -> bool {
1749        self.diffs
1750            .get(&buffer_id)
1751            .is_some_and(|diff_state| diff_state.read(cx).index_matches_head())
1752    }
1753
1754    pub fn open_conflict_set(
1755        &mut self,
1756        buffer: Entity<Buffer>,
1757        cx: &mut Context<Self>,
1758    ) -> Task<Entity<ConflictSet>> {
1759        let buffer_id = buffer.read(cx).remote_id();
1760
1761        if let Some(git_state) = self.diffs.get(&buffer_id)
1762            && let Some(conflict_set) = git_state
1763                .read(cx)
1764                .conflict_set
1765                .as_ref()
1766                .and_then(|weak| weak.upgrade())
1767        {
1768            let conflict_set = conflict_set;
1769            let buffer_snapshot = buffer.read(cx).text_snapshot();
1770
1771            let rx = git_state.update(cx, |state, cx| {
1772                state.reparse_conflict_markers(buffer_snapshot, cx)
1773            });
1774
1775            return cx.spawn(async move |_, _| {
1776                rx.await.ok();
1777                conflict_set
1778            });
1779        }
1780
1781        let is_unmerged = self
1782            .repository_and_path_for_buffer_id(buffer_id, cx)
1783            .is_some_and(|(repo, path)| repo.read(cx).snapshot.has_conflict(&path));
1784        let git_store = cx.weak_entity();
1785        let buffer_git_state = self
1786            .diffs
1787            .entry(buffer_id)
1788            .or_insert_with(|| cx.new(|cx| BufferGitState::new(git_store, cx)));
1789        let conflict_set = cx.new(|cx| ConflictSet::new(buffer_id, is_unmerged, cx));
1790
1791        self._subscriptions
1792            .push(cx.subscribe(&conflict_set, |_, _, _, cx| {
1793                cx.emit(GitStoreEvent::ConflictsUpdated);
1794            }));
1795
1796        let rx = buffer_git_state.update(cx, |state, cx| {
1797            state.conflict_set = Some(conflict_set.downgrade());
1798            let buffer_snapshot = buffer.read(cx).text_snapshot();
1799            state.reparse_conflict_markers(buffer_snapshot, cx)
1800        });
1801
1802        cx.spawn(async move |_, _| {
1803            rx.await.ok();
1804            conflict_set
1805        })
1806    }
1807
1808    pub fn project_path_git_status(
1809        &self,
1810        project_path: &ProjectPath,
1811        cx: &App,
1812    ) -> Option<FileStatus> {
1813        let (repo, repo_path) = self.repository_and_path_for_project_path(project_path, cx)?;
1814        Some(repo.read(cx).status_for_path(&repo_path)?.status)
1815    }
1816
1817    pub fn checkpoint(&self, cx: &mut App) -> Task<Result<GitStoreCheckpoint>> {
1818        let mut work_directory_abs_paths = Vec::new();
1819        let mut checkpoints = Vec::new();
1820        for repository in self.repositories.values() {
1821            repository.update(cx, |repository, _| {
1822                work_directory_abs_paths.push(repository.snapshot.work_directory_abs_path.clone());
1823                checkpoints.push(repository.checkpoint().map(|checkpoint| checkpoint?));
1824            });
1825        }
1826
1827        cx.background_executor().spawn(async move {
1828            let checkpoints = future::try_join_all(checkpoints).await?;
1829            Ok(GitStoreCheckpoint {
1830                checkpoints_by_work_dir_abs_path: work_directory_abs_paths
1831                    .into_iter()
1832                    .zip(checkpoints)
1833                    .collect(),
1834            })
1835        })
1836    }
1837
1838    pub fn restore_checkpoint(
1839        &self,
1840        checkpoint: GitStoreCheckpoint,
1841        cx: &mut App,
1842    ) -> Task<Result<()>> {
1843        let repositories_by_work_dir_abs_path = self
1844            .repositories
1845            .values()
1846            .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
1847            .collect::<HashMap<_, _>>();
1848
1849        let mut tasks = Vec::new();
1850        for (work_dir_abs_path, checkpoint) in checkpoint.checkpoints_by_work_dir_abs_path {
1851            if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) {
1852                let restore = repository.update(cx, |repository, _| {
1853                    repository.restore_checkpoint(checkpoint)
1854                });
1855                tasks.push(async move { restore.await? });
1856            }
1857        }
1858        cx.background_spawn(async move {
1859            future::try_join_all(tasks).await?;
1860            Ok(())
1861        })
1862    }
1863
1864    /// Compares two checkpoints, returning true if they are equal.
1865    pub fn compare_checkpoints(
1866        &self,
1867        left: GitStoreCheckpoint,
1868        mut right: GitStoreCheckpoint,
1869        cx: &mut App,
1870    ) -> Task<Result<bool>> {
1871        let repositories_by_work_dir_abs_path = self
1872            .repositories
1873            .values()
1874            .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
1875            .collect::<HashMap<_, _>>();
1876
1877        let mut tasks = Vec::new();
1878        for (work_dir_abs_path, left_checkpoint) in left.checkpoints_by_work_dir_abs_path {
1879            if let Some(right_checkpoint) = right
1880                .checkpoints_by_work_dir_abs_path
1881                .remove(&work_dir_abs_path)
1882            {
1883                if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
1884                {
1885                    let compare = repository.update(cx, |repository, _| {
1886                        repository.compare_checkpoints(left_checkpoint, right_checkpoint)
1887                    });
1888
1889                    tasks.push(async move { compare.await? });
1890                }
1891            } else {
1892                return Task::ready(Ok(false));
1893            }
1894        }
1895        cx.background_spawn(async move {
1896            Ok(future::try_join_all(tasks)
1897                .await?
1898                .into_iter()
1899                .all(|result| result))
1900        })
1901    }
1902
1903    /// Blames a buffer.
1904    pub fn blame_buffer(
1905        &self,
1906        buffer: &Entity<Buffer>,
1907        version: Option<clock::Global>,
1908        cx: &mut Context<Self>,
1909    ) -> Task<Result<Option<Blame>>> {
1910        let buffer = buffer.read(cx);
1911        let Some((repo, repo_path)) =
1912            self.repository_and_path_for_buffer_id(buffer.remote_id(), cx)
1913        else {
1914            return Task::ready(Err(anyhow!("failed to find a git repository for buffer")));
1915        };
1916        let content = match &version {
1917            Some(version) => buffer.rope_for_version(version),
1918            None => buffer.as_rope().clone(),
1919        };
1920        let line_ending = buffer.line_ending();
1921        let version = version.unwrap_or(buffer.version());
1922        let buffer_id = buffer.remote_id();
1923
1924        let repo = repo.downgrade();
1925        cx.spawn(async move |_, cx| {
1926            let repository_state = repo
1927                .update(cx, |repo, _| repo.repository_state.clone())?
1928                .await
1929                .map_err(|err| anyhow::anyhow!(err))?;
1930            match repository_state {
1931                RepositoryState::Local(LocalRepositoryState { backend, .. }) => backend
1932                    .blame(repo_path.clone(), content, line_ending)
1933                    .await
1934                    .with_context(|| format!("Failed to blame {:?}", repo_path.as_ref()))
1935                    .map(Some),
1936                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
1937                    let response = client
1938                        .request(proto::BlameBuffer {
1939                            project_id: project_id.to_proto(),
1940                            buffer_id: buffer_id.into(),
1941                            version: serialize_version(&version),
1942                        })
1943                        .await?;
1944                    Ok(deserialize_blame_buffer_response(response))
1945                }
1946            }
1947        })
1948    }
1949
1950    pub fn get_permalink_to_line(
1951        &self,
1952        buffer: &Entity<Buffer>,
1953        selection: Range<u32>,
1954        cx: &mut App,
1955    ) -> Task<Result<url::Url>> {
1956        let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
1957            return Task::ready(Err(anyhow!("buffer has no file")));
1958        };
1959
1960        let Some((repo, repo_path)) = self.repository_and_path_for_project_path(
1961            &(file.worktree.read(cx).id(), file.path.clone()).into(),
1962            cx,
1963        ) else {
1964            // If we're not in a Git repo, check whether this is a Rust source
1965            // file in the Cargo registry (presumably opened with go-to-definition
1966            // from a normal Rust file). If so, we can put together a permalink
1967            // using crate metadata.
1968            if buffer
1969                .read(cx)
1970                .language()
1971                .is_none_or(|lang| lang.name() != "Rust")
1972            {
1973                return Task::ready(Err(anyhow!("no permalink available")));
1974            }
1975            let file_path = file.worktree.read(cx).absolutize(&file.path);
1976            return cx.spawn(async move |cx| {
1977                let provider_registry = cx.update(GitHostingProviderRegistry::default_global);
1978                get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
1979                    .context("no permalink available")
1980            });
1981        };
1982
1983        let buffer_id = buffer.read(cx).remote_id();
1984        let branch = repo.read(cx).branch.clone();
1985        let remote = branch
1986            .as_ref()
1987            .and_then(|b| b.upstream.as_ref())
1988            .and_then(|b| b.remote_name())
1989            .unwrap_or("origin")
1990            .to_string();
1991
1992        let rx = repo.update(cx, |repo, _| {
1993            repo.send_job("get_permalink_to_line", None, move |state, cx| async move {
1994                match state {
1995                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
1996                        let origin_url = backend
1997                            .remote_url(&remote)
1998                            .await
1999                            .with_context(|| format!("remote \"{remote}\" not found"))?;
2000
2001                        let sha = backend.head_sha().await.context("reading HEAD SHA")?;
2002
2003                        let provider_registry =
2004                            cx.update(GitHostingProviderRegistry::default_global);
2005
2006                        let (provider, remote) =
2007                            parse_git_remote_url(provider_registry, &origin_url)
2008                                .context("parsing Git remote URL")?;
2009
2010                        Ok(provider.build_permalink(
2011                            remote,
2012                            BuildPermalinkParams::new(&sha, &repo_path, Some(selection)),
2013                        ))
2014                    }
2015                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
2016                        let response = client
2017                            .request(proto::GetPermalinkToLine {
2018                                project_id: project_id.to_proto(),
2019                                buffer_id: buffer_id.into(),
2020                                selection: Some(proto::Range {
2021                                    start: selection.start as u64,
2022                                    end: selection.end as u64,
2023                                }),
2024                            })
2025                            .await?;
2026
2027                        url::Url::parse(&response.permalink).context("failed to parse permalink")
2028                    }
2029                }
2030            })
2031        });
2032        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
2033    }
2034
2035    fn downstream_client(&self) -> Option<(AnyProtoClient, ProjectId)> {
2036        match &self.state {
2037            GitStoreState::Local {
2038                downstream: downstream_client,
2039                ..
2040            } => downstream_client
2041                .as_ref()
2042                .map(|state| (state.client.clone(), state.project_id)),
2043            GitStoreState::Remote {
2044                downstream: downstream_client,
2045                ..
2046            } => downstream_client.clone(),
2047        }
2048    }
2049
2050    fn upstream_client(&self) -> Option<AnyProtoClient> {
2051        match &self.state {
2052            GitStoreState::Local { .. } => None,
2053            GitStoreState::Remote {
2054                upstream_client, ..
2055            } => Some(upstream_client.clone()),
2056        }
2057    }
2058
2059    fn on_worktree_store_event(
2060        &mut self,
2061        worktree_store: Entity<WorktreeStore>,
2062        event: &WorktreeStoreEvent,
2063        cx: &mut Context<Self>,
2064    ) {
2065        let GitStoreState::Local {
2066            project_environment,
2067            downstream,
2068            next_repository_id,
2069            fs,
2070            ..
2071        } = &self.state
2072        else {
2073            return;
2074        };
2075
2076        match event {
2077            WorktreeStoreEvent::WorktreeUpdatedEntries(worktree_id, updated_entries) => {
2078                if let Some(worktree) = self
2079                    .worktree_store
2080                    .read(cx)
2081                    .worktree_for_id(*worktree_id, cx)
2082                {
2083                    let paths_by_git_repo =
2084                        self.process_updated_entries(&worktree, updated_entries, cx);
2085                    let downstream = downstream
2086                        .as_ref()
2087                        .map(|downstream| downstream.updates_tx.clone());
2088                    cx.spawn(async move |_, cx| {
2089                        let paths_by_git_repo = paths_by_git_repo.await;
2090                        for (repo, paths) in paths_by_git_repo {
2091                            repo.update(cx, |repo, cx| {
2092                                repo.paths_changed(paths, downstream.clone(), cx);
2093                            });
2094                        }
2095                    })
2096                    .detach();
2097                }
2098            }
2099            WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => {
2100                let Some(worktree) = worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
2101                else {
2102                    return;
2103                };
2104                log::debug!("received worktree update for repositories: {changed_repos:?}");
2105                self.update_repositories_from_worktree(
2106                    *worktree_id,
2107                    project_environment.clone(),
2108                    next_repository_id.clone(),
2109                    downstream
2110                        .as_ref()
2111                        .map(|downstream| downstream.updates_tx.clone()),
2112                    changed_repos.clone(),
2113                    fs.clone(),
2114                    cx,
2115                );
2116                self.local_worktree_git_repos_changed(worktree, changed_repos, cx);
2117            }
2118            WorktreeStoreEvent::WorktreeRemoved(_entity_id, worktree_id) => {
2119                let repos_without_worktree: Vec<RepositoryId> = self
2120                    .worktree_ids
2121                    .iter_mut()
2122                    .filter_map(|(repo_id, worktree_ids)| {
2123                        worktree_ids.remove(worktree_id);
2124                        if worktree_ids.is_empty() {
2125                            Some(*repo_id)
2126                        } else {
2127                            None
2128                        }
2129                    })
2130                    .collect();
2131                let is_active_repo_removed = repos_without_worktree
2132                    .iter()
2133                    .any(|repo_id| self.active_repo_id == Some(*repo_id));
2134
2135                for repo_id in repos_without_worktree {
2136                    self.repositories.remove(&repo_id);
2137                    self.worktree_ids.remove(&repo_id);
2138                    if let Some(updates_tx) =
2139                        downstream.as_ref().map(|downstream| &downstream.updates_tx)
2140                    {
2141                        updates_tx
2142                            .unbounded_send(DownstreamUpdate::RemoveRepository(repo_id))
2143                            .ok();
2144                    }
2145                }
2146
2147                if is_active_repo_removed {
2148                    if let Some((&repo_id, _)) = self.repositories.iter().next() {
2149                        self.active_repo_id = Some(repo_id);
2150                        cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(repo_id)));
2151                    } else {
2152                        self.active_repo_id = None;
2153                        cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
2154                    }
2155                }
2156            }
2157            _ => {}
2158        }
2159    }
2160    fn on_repository_event(
2161        &mut self,
2162        repo: Entity<Repository>,
2163        event: &RepositoryEvent,
2164        cx: &mut Context<Self>,
2165    ) {
2166        let id = repo.read(cx).id;
2167        let repo_snapshot = repo.read(cx).snapshot.clone();
2168        for (buffer_id, diff) in self.diffs.iter() {
2169            if let Some((buffer_repo, repo_path)) =
2170                self.repository_and_path_for_buffer_id(*buffer_id, cx)
2171                && buffer_repo == repo
2172            {
2173                diff.update(cx, |diff, cx| {
2174                    if let Some(conflict_set) = &diff.conflict_set {
2175                        let conflict_status_changed =
2176                            conflict_set.update(cx, |conflict_set, cx| {
2177                                let has_conflict = repo_snapshot.has_conflict(&repo_path);
2178                                conflict_set.set_has_conflict(has_conflict, cx)
2179                            })?;
2180                        if conflict_status_changed {
2181                            let buffer_store = self.buffer_store.read(cx);
2182                            if let Some(buffer) = buffer_store.get(*buffer_id) {
2183                                let _ = diff
2184                                    .reparse_conflict_markers(buffer.read(cx).text_snapshot(), cx);
2185                            }
2186                        }
2187                    }
2188                    anyhow::Ok(())
2189                })
2190                .ok();
2191            }
2192        }
2193        cx.emit(GitStoreEvent::RepositoryUpdated(
2194            id,
2195            event.clone(),
2196            self.active_repo_id == Some(id),
2197        ))
2198    }
2199
2200    fn on_jobs_updated(&mut self, _: Entity<Repository>, _: &JobsUpdated, cx: &mut Context<Self>) {
2201        cx.emit(GitStoreEvent::JobsUpdated)
2202    }
2203
2204    fn repository_is_trusted(&self, repository_id: RepositoryId, cx: &mut Context<Self>) -> bool {
2205        let Some(worktree_ids) = self.worktree_ids.get(&repository_id) else {
2206            return false;
2207        };
2208        let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) else {
2209            return false;
2210        };
2211
2212        worktree_ids.iter().any(|worktree_id| {
2213            trusted_worktrees.update(cx, |trusted_worktrees, cx| {
2214                trusted_worktrees.can_trust(&self.worktree_store, *worktree_id, cx)
2215            })
2216        })
2217    }
2218
2219    /// Update our list of repositories and schedule git scans in response to a notification from a worktree,
2220    fn update_repositories_from_worktree(
2221        &mut self,
2222        worktree_id: WorktreeId,
2223        project_environment: Entity<ProjectEnvironment>,
2224        next_repository_id: Arc<AtomicU64>,
2225        updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
2226        updated_git_repositories: UpdatedGitRepositoriesSet,
2227        fs: Arc<dyn Fs>,
2228        cx: &mut Context<Self>,
2229    ) {
2230        let mut removed_ids = Vec::new();
2231
2232        let is_trusted = TrustedWorktrees::try_get_global(cx)
2233            .map(|trusted_worktrees| {
2234                trusted_worktrees.update(cx, |trusted_worktrees, cx| {
2235                    trusted_worktrees.can_trust(&self.worktree_store, worktree_id, cx)
2236                })
2237            })
2238            .unwrap_or(false);
2239
2240        for update in updated_git_repositories.iter() {
2241            if let Some((id, existing)) = self.repositories.iter().find(|(_, repo)| {
2242                let existing_work_directory_abs_path = &repo.read(cx).work_directory_abs_path;
2243                Some(existing_work_directory_abs_path)
2244                    == update.old_work_directory_abs_path.as_ref()
2245                    || Some(existing_work_directory_abs_path)
2246                        == update.new_work_directory_abs_path.as_ref()
2247            }) {
2248                let repo_id = *id;
2249                if let Some(new_work_directory_abs_path) =
2250                    update.new_work_directory_abs_path.clone()
2251                {
2252                    self.worktree_ids
2253                        .entry(repo_id)
2254                        .or_insert_with(HashSet::new)
2255                        .insert(worktree_id);
2256                    let path_changed = update.old_work_directory_abs_path.as_ref()
2257                        != update.new_work_directory_abs_path.as_ref();
2258                    if path_changed
2259                        && let Some(dot_git_abs_path) = update.dot_git_abs_path.clone()
2260                        && let Some(repository_dir_abs_path) =
2261                            update.repository_dir_abs_path.clone()
2262                        && let Some(common_dir_abs_path) = update.common_dir_abs_path.clone()
2263                    {
2264                        existing.update(cx, |existing, cx| {
2265                            existing.reinitialize_local_backend(
2266                                new_work_directory_abs_path,
2267                                dot_git_abs_path,
2268                                repository_dir_abs_path,
2269                                common_dir_abs_path,
2270                                project_environment.downgrade(),
2271                                fs.clone(),
2272                                is_trusted,
2273                                cx,
2274                            );
2275                            existing.schedule_scan(updates_tx.clone(), cx);
2276                        });
2277                    } else {
2278                        existing.update(cx, |existing, cx| {
2279                            existing.snapshot.work_directory_abs_path = new_work_directory_abs_path;
2280                            existing.schedule_scan(updates_tx.clone(), cx);
2281                        });
2282                    }
2283                } else {
2284                    if let Some(worktree_ids) = self.worktree_ids.get_mut(&repo_id) {
2285                        worktree_ids.remove(&worktree_id);
2286                        if worktree_ids.is_empty() {
2287                            removed_ids.push(repo_id);
2288                        }
2289                    }
2290                }
2291            } else if let UpdatedGitRepository {
2292                new_work_directory_abs_path: Some(work_directory_abs_path),
2293                dot_git_abs_path: Some(dot_git_abs_path),
2294                repository_dir_abs_path: Some(repository_dir_abs_path),
2295                common_dir_abs_path: Some(common_dir_abs_path),
2296                ..
2297            } = update
2298            {
2299                let id = RepositoryId(next_repository_id.fetch_add(1, atomic::Ordering::Release));
2300                let git_store = cx.weak_entity();
2301                let repo = cx.new(|cx| {
2302                    let mut repo = Repository::local(
2303                        id,
2304                        work_directory_abs_path.clone(),
2305                        repository_dir_abs_path.clone(),
2306                        common_dir_abs_path.clone(),
2307                        dot_git_abs_path.clone(),
2308                        project_environment.downgrade(),
2309                        fs.clone(),
2310                        is_trusted,
2311                        git_store,
2312                        cx,
2313                    );
2314                    if let Some(updates_tx) = updates_tx.as_ref() {
2315                        // trigger an empty `UpdateRepository` to ensure remote active_repo_id is set correctly
2316                        updates_tx
2317                            .unbounded_send(DownstreamUpdate::UpdateRepository(repo.snapshot()))
2318                            .ok();
2319                    }
2320                    repo.schedule_scan(updates_tx.clone(), cx);
2321                    repo
2322                });
2323                self._subscriptions
2324                    .push(cx.subscribe(&repo, Self::on_repository_event));
2325                self._subscriptions
2326                    .push(cx.subscribe(&repo, Self::on_jobs_updated));
2327                self.repositories.insert(id, repo);
2328                self.worktree_ids.insert(id, HashSet::from([worktree_id]));
2329                cx.emit(GitStoreEvent::RepositoryAdded);
2330                self.active_repo_id.get_or_insert_with(|| {
2331                    cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
2332                    id
2333                });
2334            }
2335        }
2336
2337        for id in removed_ids {
2338            if self.active_repo_id == Some(id) {
2339                self.active_repo_id = None;
2340                cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
2341            }
2342            self.repositories.remove(&id);
2343            if let Some(updates_tx) = updates_tx.as_ref() {
2344                updates_tx
2345                    .unbounded_send(DownstreamUpdate::RemoveRepository(id))
2346                    .ok();
2347            }
2348        }
2349    }
2350
2351    fn on_trusted_worktrees_event(
2352        &mut self,
2353        _: Entity<TrustedWorktreesStore>,
2354        event: &TrustedWorktreesEvent,
2355        cx: &mut Context<Self>,
2356    ) {
2357        if !matches!(self.state, GitStoreState::Local { .. }) {
2358            return;
2359        }
2360
2361        let (is_trusted, event_paths) = match event {
2362            TrustedWorktreesEvent::Trusted(_, trusted_paths) => (true, trusted_paths),
2363            TrustedWorktreesEvent::Restricted(_, restricted_paths) => (false, restricted_paths),
2364        };
2365
2366        for (repo_id, worktree_ids) in &self.worktree_ids {
2367            if worktree_ids
2368                .iter()
2369                .any(|worktree_id| event_paths.contains(&PathTrust::Worktree(*worktree_id)))
2370            {
2371                if let Some(repo) = self.repositories.get(repo_id) {
2372                    let repository_state = repo.read(cx).repository_state.clone();
2373                    cx.background_spawn(async move {
2374                        if let Ok(RepositoryState::Local(state)) = repository_state.await {
2375                            state.backend.set_trusted(is_trusted);
2376                        }
2377                    })
2378                    .detach();
2379                }
2380            }
2381        }
2382    }
2383
2384    fn on_buffer_store_event(
2385        &mut self,
2386        _: Entity<BufferStore>,
2387        event: &BufferStoreEvent,
2388        cx: &mut Context<Self>,
2389    ) {
2390        match event {
2391            BufferStoreEvent::BufferAdded(buffer) => {
2392                cx.subscribe(buffer, |this, buffer, event, cx| {
2393                    if let BufferEvent::LanguageChanged(_) = event {
2394                        let buffer_id = buffer.read(cx).remote_id();
2395                        if let Some(diff_state) = this.diffs.get(&buffer_id) {
2396                            diff_state.update(cx, |diff_state, cx| {
2397                                diff_state.buffer_language_changed(buffer, cx);
2398                            });
2399                        }
2400                    }
2401                })
2402                .detach();
2403            }
2404            BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id) => {
2405                if let Some(diffs) = self.shared_diffs.get_mut(peer_id) {
2406                    diffs.remove(buffer_id);
2407                }
2408            }
2409            BufferStoreEvent::BufferDropped(buffer_id) => {
2410                self.diffs.remove(buffer_id);
2411                self.buffer_ids_by_index_text_buffer_id
2412                    .retain(|_, main_buffer_id| main_buffer_id != buffer_id);
2413                for diffs in self.shared_diffs.values_mut() {
2414                    diffs.remove(buffer_id);
2415                }
2416            }
2417            BufferStoreEvent::BufferChangedFilePath { buffer, .. } => {
2418                // Whenever a buffer's file path changes, it's possible that the
2419                // new path is actually a path that is being tracked by a git
2420                // repository. In that case, we'll want to update the buffer's
2421                // `BufferDiffState`, in case it already has one.
2422                let buffer_id = buffer.read(cx).remote_id();
2423                let diff_state = self.diffs.get(&buffer_id);
2424                let repo = self.repository_and_path_for_buffer_id(buffer_id, cx);
2425
2426                if let Some(diff_state) = diff_state
2427                    && let Some((repo, repo_path)) = repo
2428                {
2429                    let buffer = buffer.clone();
2430                    let diff_state = diff_state.clone();
2431                    let is_symlink = Self::buffer_is_symlink(&buffer, cx);
2432
2433                    cx.spawn(async move |_git_store, cx| {
2434                        async {
2435                            let diff_bases_change = if is_symlink {
2436                                DiffBasesChange::SetBoth(None)
2437                            } else {
2438                                repo.update(cx, |repo, cx| {
2439                                    repo.load_committed_text(buffer_id, repo_path, cx)
2440                                })
2441                                .await?
2442                            };
2443
2444                            diff_state.update(cx, |diff_state, cx| {
2445                                let buffer_snapshot = buffer.read(cx).text_snapshot();
2446                                diff_state.diff_bases_changed(
2447                                    buffer_snapshot,
2448                                    Some(diff_bases_change),
2449                                    cx,
2450                                );
2451                            });
2452                            anyhow::Ok(())
2453                        }
2454                        .await
2455                        .log_err();
2456                    })
2457                    .detach();
2458                }
2459            }
2460        }
2461    }
2462
2463    pub fn recalculate_buffer_diffs(
2464        &mut self,
2465        buffers: Vec<Entity<Buffer>>,
2466        cx: &mut Context<Self>,
2467    ) -> impl Future<Output = ()> + use<> {
2468        let mut futures = Vec::new();
2469        for buffer in buffers {
2470            if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) {
2471                let buffer = buffer.read(cx).text_snapshot();
2472                diff_state.update(cx, |diff_state, cx| {
2473                    diff_state.recalculate_diffs(buffer.clone(), cx);
2474                    futures.extend(diff_state.wait_for_recalculation().map(FutureExt::boxed));
2475                });
2476                futures.push(diff_state.update(cx, |diff_state, cx| {
2477                    diff_state
2478                        .reparse_conflict_markers(buffer, cx)
2479                        .map(|_| {})
2480                        .boxed()
2481                }));
2482            }
2483        }
2484        async move {
2485            futures::future::join_all(futures).await;
2486        }
2487    }
2488
2489    fn write_index_text_for_buffer_id(
2490        &mut self,
2491        buffer_id: BufferId,
2492        new_index_text: Option<String>,
2493        cx: &mut Context<Self>,
2494    ) {
2495        let Some(diff_state) = self.diffs.get(&buffer_id) else {
2496            return;
2497        };
2498        let hunk_staging_operation_count = diff_state.update(cx, |diff_state, _| {
2499            diff_state.hunk_staging_operation_count += 1;
2500            diff_state.hunk_staging_operation_count
2501        });
2502        let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) else {
2503            return;
2504        };
2505        let recv = repo.update(cx, |repo, cx| {
2506            log::debug!("hunks changed for {}", path.as_unix_str());
2507            repo.spawn_set_index_text_job(
2508                path,
2509                new_index_text,
2510                Some(hunk_staging_operation_count),
2511                cx,
2512            )
2513        });
2514        cx.spawn(async move |this, cx| {
2515            if let Ok(Err(error)) = cx.background_spawn(recv).await {
2516                this.update(cx, |this, cx| {
2517                    if let Some(diff_state) = this.diffs.get(&buffer_id).cloned() {
2518                        diff_state.update(cx, |diff_state, cx| {
2519                            diff_state.clear_pending_index_edits_and_hunks(cx);
2520                        });
2521                    }
2522                    cx.emit(GitStoreEvent::IndexWriteError(error));
2523                })
2524                .ok();
2525            }
2526        })
2527        .detach();
2528    }
2529
2530    fn local_worktree_git_repos_changed(
2531        &mut self,
2532        worktree: Entity<Worktree>,
2533        changed_repos: &UpdatedGitRepositoriesSet,
2534        cx: &mut Context<Self>,
2535    ) {
2536        log::debug!("local worktree repos changed");
2537        debug_assert!(worktree.read(cx).is_local());
2538
2539        for repository in self.repositories.values() {
2540            repository.update(cx, |repository, cx| {
2541                let repo_abs_path = &repository.work_directory_abs_path;
2542                if changed_repos.iter().any(|update| {
2543                    update.old_work_directory_abs_path.as_ref() == Some(repo_abs_path)
2544                        || update.new_work_directory_abs_path.as_ref() == Some(repo_abs_path)
2545                }) {
2546                    repository.reload_buffer_diff_bases(cx);
2547                    cx.emit(RepositoryEvent::GitDirectoryChanged);
2548                }
2549            });
2550        }
2551    }
2552
2553    pub fn repositories(&self) -> &HashMap<RepositoryId, Entity<Repository>> {
2554        &self.repositories
2555    }
2556
2557    /// Returns the main repository working directory for the given worktree.
2558    /// For normal checkouts this equals the worktree's own path. For linked
2559    /// worktrees it points back to the main worktree, if one exists. Linked
2560    /// worktrees attached to a bare repository have no main worktree path.
2561    pub fn original_repo_path_for_worktree(
2562        &self,
2563        worktree_id: WorktreeId,
2564        cx: &App,
2565    ) -> Option<Arc<Path>> {
2566        self.active_repo_id
2567            .iter()
2568            .chain(self.worktree_ids.keys())
2569            .find(|repo_id| {
2570                self.worktree_ids
2571                    .get(repo_id)
2572                    .is_some_and(|ids| ids.contains(&worktree_id))
2573            })
2574            .and_then(|repo_id| self.repositories.get(repo_id))
2575            .and_then(|repo| {
2576                repo.read(cx)
2577                    .snapshot()
2578                    .main_worktree_abs_path()
2579                    .map(Arc::from)
2580            })
2581    }
2582
2583    pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
2584        let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?;
2585        let status = repo.read(cx).snapshot.status_for_path(&path)?;
2586        Some(status.status)
2587    }
2588
2589    pub fn repository_and_path_for_buffer_id(
2590        &self,
2591        buffer_id: BufferId,
2592        cx: &App,
2593    ) -> Option<(Entity<Repository>, RepoPath)> {
2594        let buffer = self.buffer_store.read(cx).get(buffer_id)?;
2595        let project_path = buffer.read(cx).project_path(cx)?;
2596        self.repository_and_path_for_project_path(&project_path, cx)
2597    }
2598
2599    pub fn repository_and_path_for_project_path(
2600        &self,
2601        path: &ProjectPath,
2602        cx: &App,
2603    ) -> Option<(Entity<Repository>, RepoPath)> {
2604        let abs_path = self.worktree_store.read(cx).absolutize(path, cx)?;
2605        self.repositories
2606            .values()
2607            .filter_map(|repo| {
2608                let repo_path = repo.read(cx).abs_path_to_repo_path(&abs_path)?;
2609                Some((repo.clone(), repo_path))
2610            })
2611            .max_by_key(|(repo, _)| repo.read(cx).work_directory_abs_path.clone())
2612    }
2613
2614    pub fn git_init(
2615        &self,
2616        path: Arc<Path>,
2617        fallback_branch_name: String,
2618        cx: &App,
2619    ) -> Task<Result<()>> {
2620        match &self.state {
2621            GitStoreState::Local { fs, .. } => {
2622                let fs = fs.clone();
2623                cx.background_executor()
2624                    .spawn(async move { fs.git_init(&path, fallback_branch_name).await })
2625            }
2626            GitStoreState::Remote {
2627                upstream_client,
2628                upstream_project_id: project_id,
2629                ..
2630            } => {
2631                let client = upstream_client.clone();
2632                let project_id = *project_id;
2633                cx.background_executor().spawn(async move {
2634                    client
2635                        .request(proto::GitInit {
2636                            project_id: project_id,
2637                            abs_path: path.to_string_lossy().into_owned(),
2638                            fallback_branch_name,
2639                        })
2640                        .await?;
2641                    Ok(())
2642                })
2643            }
2644        }
2645    }
2646
2647    pub fn git_clone(
2648        &self,
2649        repo: String,
2650        path: impl Into<Arc<std::path::Path>>,
2651        cx: &App,
2652    ) -> Task<Result<()>> {
2653        let path = path.into();
2654        match &self.state {
2655            GitStoreState::Local { fs, .. } => {
2656                let fs = fs.clone();
2657                cx.background_executor()
2658                    .spawn(async move { fs.git_clone(&path, &repo).await })
2659            }
2660            GitStoreState::Remote {
2661                upstream_client,
2662                upstream_project_id,
2663                ..
2664            } => {
2665                if upstream_client.is_via_collab() {
2666                    return Task::ready(Err(anyhow!(
2667                        "Git Clone isn't supported for project guests"
2668                    )));
2669                }
2670                let request = upstream_client.request(proto::GitClone {
2671                    project_id: *upstream_project_id,
2672                    abs_path: path.to_string_lossy().into_owned(),
2673                    remote_repo: repo,
2674                });
2675
2676                cx.background_spawn(async move {
2677                    let result = request.await?;
2678
2679                    match result.success {
2680                        true => Ok(()),
2681                        false => Err(anyhow!("Git Clone failed")),
2682                    }
2683                })
2684            }
2685        }
2686    }
2687
2688    pub fn git_config(&self, path: Arc<Path>, args: Vec<String>, cx: &App) -> Task<Result<String>> {
2689        match &self.state {
2690            GitStoreState::Local { fs, .. } => {
2691                let fs = fs.clone();
2692                cx.background_executor()
2693                    .spawn(async move { fs.git_config(&path, args).await })
2694            }
2695            GitStoreState::Remote {
2696                upstream_client, ..
2697            } => {
2698                // Prevent running git config commands for collab.
2699                if upstream_client.is_via_collab() {
2700                    return Task::ready(Err(anyhow!(
2701                        "Git Config isn't support for project guests"
2702                    )));
2703                }
2704
2705                // TODO: Implement this for remote repositories.
2706                Task::ready(Err(anyhow!(
2707                    "Git Config isn't yet supported for remote projects"
2708                )))
2709            }
2710        }
2711    }
2712
2713    async fn handle_update_repository(
2714        this: Entity<Self>,
2715        envelope: TypedEnvelope<proto::UpdateRepository>,
2716        mut cx: AsyncApp,
2717    ) -> Result<()> {
2718        this.update(&mut cx, |this, cx| {
2719            let path_style = this.worktree_store.read(cx).path_style();
2720            let mut update = envelope.payload;
2721
2722            let id = RepositoryId::from_proto(update.id);
2723            let client = this.upstream_client().context("no upstream client")?;
2724
2725            let repository_dir_abs_path: Option<Arc<Path>> = update
2726                .repository_dir_abs_path
2727                .as_deref()
2728                .map(|p| Path::new(p).into());
2729            let common_dir_abs_path: Option<Arc<Path>> = update
2730                .common_dir_abs_path
2731                .as_deref()
2732                .map(|p| Path::new(p).into());
2733
2734            let mut repo_subscription = None;
2735            let repo = this.repositories.entry(id).or_insert_with(|| {
2736                let git_store = cx.weak_entity();
2737                let repo = cx.new(|cx| {
2738                    Repository::remote(
2739                        id,
2740                        Path::new(&update.abs_path).into(),
2741                        repository_dir_abs_path.clone(),
2742                        common_dir_abs_path.clone(),
2743                        path_style,
2744                        ProjectId(update.project_id),
2745                        client,
2746                        git_store,
2747                        cx,
2748                    )
2749                });
2750                repo_subscription = Some(cx.subscribe(&repo, Self::on_repository_event));
2751                cx.emit(GitStoreEvent::RepositoryAdded);
2752                repo
2753            });
2754            this._subscriptions.extend(repo_subscription);
2755
2756            repo.update(cx, {
2757                let update = update.clone();
2758                |repo, cx| repo.apply_remote_update(update, cx)
2759            })?;
2760
2761            this.active_repo_id.get_or_insert_with(|| {
2762                cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
2763                id
2764            });
2765
2766            if let Some((client, project_id)) = this.downstream_client() {
2767                update.project_id = project_id.to_proto();
2768                client.send(update).log_err();
2769            }
2770            Ok(())
2771        })
2772    }
2773
2774    async fn handle_remove_repository(
2775        this: Entity<Self>,
2776        envelope: TypedEnvelope<proto::RemoveRepository>,
2777        mut cx: AsyncApp,
2778    ) -> Result<()> {
2779        this.update(&mut cx, |this, cx| {
2780            let mut update = envelope.payload;
2781            let id = RepositoryId::from_proto(update.id);
2782            this.repositories.remove(&id);
2783            if let Some((client, project_id)) = this.downstream_client() {
2784                update.project_id = project_id.to_proto();
2785                client.send(update).log_err();
2786            }
2787            if this.active_repo_id == Some(id) {
2788                this.active_repo_id = None;
2789                cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
2790            }
2791            cx.emit(GitStoreEvent::RepositoryRemoved(id));
2792        });
2793        Ok(())
2794    }
2795
2796    async fn handle_git_init(
2797        this: Entity<Self>,
2798        envelope: TypedEnvelope<proto::GitInit>,
2799        cx: AsyncApp,
2800    ) -> Result<proto::Ack> {
2801        let path: Arc<Path> = PathBuf::from(envelope.payload.abs_path).into();
2802        let name = envelope.payload.fallback_branch_name;
2803        cx.update(|cx| this.read(cx).git_init(path, name, cx))
2804            .await?;
2805
2806        Ok(proto::Ack {})
2807    }
2808
2809    async fn handle_git_clone(
2810        this: Entity<Self>,
2811        envelope: TypedEnvelope<proto::GitClone>,
2812        cx: AsyncApp,
2813    ) -> Result<proto::GitCloneResponse> {
2814        let path: Arc<Path> = PathBuf::from(envelope.payload.abs_path).into();
2815        let repo_name = envelope.payload.remote_repo;
2816        let result = cx
2817            .update(|cx| this.read(cx).git_clone(repo_name, path, cx))
2818            .await;
2819
2820        Ok(proto::GitCloneResponse {
2821            success: result.is_ok(),
2822        })
2823    }
2824
2825    async fn handle_fetch(
2826        this: Entity<Self>,
2827        envelope: TypedEnvelope<proto::Fetch>,
2828        mut cx: AsyncApp,
2829    ) -> Result<proto::RemoteMessageResponse> {
2830        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2831        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2832        let fetch_options = FetchOptions::from_proto(envelope.payload.remote);
2833        let askpass_id = envelope.payload.askpass_id;
2834
2835        let askpass = make_remote_delegate(
2836            this,
2837            envelope.payload.project_id,
2838            repository_id,
2839            askpass_id,
2840            &mut cx,
2841        );
2842
2843        let remote_output = repository_handle
2844            .update(&mut cx, |repository_handle, cx| {
2845                repository_handle.fetch(fetch_options, askpass, cx)
2846            })
2847            .await??;
2848
2849        Ok(proto::RemoteMessageResponse {
2850            stdout: remote_output.stdout,
2851            stderr: remote_output.stderr,
2852        })
2853    }
2854
2855    async fn handle_push(
2856        this: Entity<Self>,
2857        envelope: TypedEnvelope<proto::Push>,
2858        mut cx: AsyncApp,
2859    ) -> Result<proto::RemoteMessageResponse> {
2860        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2861        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2862
2863        let askpass_id = envelope.payload.askpass_id;
2864        let askpass = make_remote_delegate(
2865            this,
2866            envelope.payload.project_id,
2867            repository_id,
2868            askpass_id,
2869            &mut cx,
2870        );
2871
2872        let options = envelope
2873            .payload
2874            .options
2875            .as_ref()
2876            .map(|_| match envelope.payload.options() {
2877                proto::push::PushOptions::SetUpstream => git::repository::PushOptions::SetUpstream,
2878                proto::push::PushOptions::Force => git::repository::PushOptions::Force,
2879            });
2880
2881        let branch_name = envelope.payload.branch_name.into();
2882        let remote_branch_name = envelope.payload.remote_branch_name.into();
2883        let remote_name = envelope.payload.remote_name.into();
2884
2885        let remote_output = repository_handle
2886            .update(&mut cx, |repository_handle, cx| {
2887                repository_handle.push(
2888                    branch_name,
2889                    remote_branch_name,
2890                    remote_name,
2891                    options,
2892                    askpass,
2893                    cx,
2894                )
2895            })
2896            .await??;
2897        Ok(proto::RemoteMessageResponse {
2898            stdout: remote_output.stdout,
2899            stderr: remote_output.stderr,
2900        })
2901    }
2902
2903    async fn handle_pull(
2904        this: Entity<Self>,
2905        envelope: TypedEnvelope<proto::Pull>,
2906        mut cx: AsyncApp,
2907    ) -> Result<proto::RemoteMessageResponse> {
2908        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2909        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2910        let askpass_id = envelope.payload.askpass_id;
2911        let askpass = make_remote_delegate(
2912            this,
2913            envelope.payload.project_id,
2914            repository_id,
2915            askpass_id,
2916            &mut cx,
2917        );
2918
2919        let branch_name = envelope.payload.branch_name.map(|name| name.into());
2920        let remote_name = envelope.payload.remote_name.into();
2921        let rebase = envelope.payload.rebase;
2922
2923        let remote_message = repository_handle
2924            .update(&mut cx, |repository_handle, cx| {
2925                repository_handle.pull(branch_name, remote_name, rebase, askpass, cx)
2926            })
2927            .await??;
2928
2929        Ok(proto::RemoteMessageResponse {
2930            stdout: remote_message.stdout,
2931            stderr: remote_message.stderr,
2932        })
2933    }
2934
2935    async fn handle_stage(
2936        this: Entity<Self>,
2937        envelope: TypedEnvelope<proto::Stage>,
2938        mut cx: AsyncApp,
2939    ) -> Result<proto::Ack> {
2940        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2941        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2942
2943        let entries = envelope
2944            .payload
2945            .paths
2946            .into_iter()
2947            .map(|path| RepoPath::new(&path))
2948            .collect::<Result<Vec<_>>>()?;
2949
2950        repository_handle
2951            .update(&mut cx, |repository_handle, cx| {
2952                repository_handle.stage_entries(entries, cx)
2953            })
2954            .await?;
2955        Ok(proto::Ack {})
2956    }
2957
2958    async fn handle_unstage(
2959        this: Entity<Self>,
2960        envelope: TypedEnvelope<proto::Unstage>,
2961        mut cx: AsyncApp,
2962    ) -> Result<proto::Ack> {
2963        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2964        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2965
2966        let entries = envelope
2967            .payload
2968            .paths
2969            .into_iter()
2970            .map(|path| RepoPath::new(&path))
2971            .collect::<Result<Vec<_>>>()?;
2972
2973        repository_handle
2974            .update(&mut cx, |repository_handle, cx| {
2975                repository_handle.unstage_entries(entries, cx)
2976            })
2977            .await?;
2978
2979        Ok(proto::Ack {})
2980    }
2981
2982    async fn handle_stash(
2983        this: Entity<Self>,
2984        envelope: TypedEnvelope<proto::Stash>,
2985        mut cx: AsyncApp,
2986    ) -> Result<proto::Ack> {
2987        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2988        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2989
2990        let entries = envelope
2991            .payload
2992            .paths
2993            .into_iter()
2994            .map(|path| RepoPath::new(&path))
2995            .collect::<Result<Vec<_>>>()?;
2996
2997        repository_handle
2998            .update(&mut cx, |repository_handle, cx| {
2999                repository_handle.stash_entries(entries, cx)
3000            })
3001            .await?;
3002
3003        Ok(proto::Ack {})
3004    }
3005
3006    async fn handle_stash_pop(
3007        this: Entity<Self>,
3008        envelope: TypedEnvelope<proto::StashPop>,
3009        mut cx: AsyncApp,
3010    ) -> Result<proto::Ack> {
3011        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3012        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3013        let stash_index = envelope.payload.stash_index.map(|i| i as usize);
3014
3015        repository_handle
3016            .update(&mut cx, |repository_handle, cx| {
3017                repository_handle.stash_pop(stash_index, cx)
3018            })
3019            .await?;
3020
3021        Ok(proto::Ack {})
3022    }
3023
3024    async fn handle_stash_apply(
3025        this: Entity<Self>,
3026        envelope: TypedEnvelope<proto::StashApply>,
3027        mut cx: AsyncApp,
3028    ) -> Result<proto::Ack> {
3029        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3030        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3031        let stash_index = envelope.payload.stash_index.map(|i| i as usize);
3032
3033        repository_handle
3034            .update(&mut cx, |repository_handle, cx| {
3035                repository_handle.stash_apply(stash_index, cx)
3036            })
3037            .await?;
3038
3039        Ok(proto::Ack {})
3040    }
3041
3042    async fn handle_stash_drop(
3043        this: Entity<Self>,
3044        envelope: TypedEnvelope<proto::StashDrop>,
3045        mut cx: AsyncApp,
3046    ) -> Result<proto::Ack> {
3047        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3048        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3049        let stash_index = envelope.payload.stash_index.map(|i| i as usize);
3050
3051        repository_handle
3052            .update(&mut cx, |repository_handle, cx| {
3053                repository_handle.stash_drop(stash_index, cx)
3054            })
3055            .await??;
3056
3057        Ok(proto::Ack {})
3058    }
3059
3060    async fn handle_set_index_text(
3061        this: Entity<Self>,
3062        envelope: TypedEnvelope<proto::SetIndexText>,
3063        mut cx: AsyncApp,
3064    ) -> Result<proto::Ack> {
3065        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3066        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3067        let repo_path = RepoPath::from_proto(&envelope.payload.path)?;
3068
3069        repository_handle
3070            .update(&mut cx, |repository_handle, cx| {
3071                repository_handle.spawn_set_index_text_job(
3072                    repo_path,
3073                    envelope.payload.text,
3074                    None,
3075                    cx,
3076                )
3077            })
3078            .await??;
3079        Ok(proto::Ack {})
3080    }
3081
3082    async fn handle_run_hook(
3083        this: Entity<Self>,
3084        envelope: TypedEnvelope<proto::RunGitHook>,
3085        mut cx: AsyncApp,
3086    ) -> Result<proto::Ack> {
3087        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3088        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3089        let hook = RunHook::from_proto(envelope.payload.hook).context("invalid hook")?;
3090        repository_handle
3091            .update(&mut cx, |repository_handle, cx| {
3092                repository_handle.run_hook(hook, cx)
3093            })
3094            .await??;
3095        Ok(proto::Ack {})
3096    }
3097
3098    async fn handle_commit(
3099        this: Entity<Self>,
3100        envelope: TypedEnvelope<proto::Commit>,
3101        mut cx: AsyncApp,
3102    ) -> Result<proto::Ack> {
3103        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3104        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3105        let askpass_id = envelope.payload.askpass_id;
3106
3107        let askpass = make_remote_delegate(
3108            this,
3109            envelope.payload.project_id,
3110            repository_id,
3111            askpass_id,
3112            &mut cx,
3113        );
3114
3115        let message = SharedString::from(envelope.payload.message);
3116        let name = envelope.payload.name.map(SharedString::from);
3117        let email = envelope.payload.email.map(SharedString::from);
3118        let options = envelope.payload.options.unwrap_or_default();
3119
3120        repository_handle
3121            .update(&mut cx, |repository_handle, cx| {
3122                repository_handle.commit(
3123                    message,
3124                    name.zip(email),
3125                    CommitOptions {
3126                        amend: options.amend,
3127                        signoff: options.signoff,
3128                        allow_empty: options.allow_empty,
3129                        no_verify: options.no_verify,
3130                    },
3131                    askpass,
3132                    cx,
3133                )
3134            })
3135            .await??;
3136        Ok(proto::Ack {})
3137    }
3138
3139    async fn handle_get_remotes(
3140        this: Entity<Self>,
3141        envelope: TypedEnvelope<proto::GetRemotes>,
3142        mut cx: AsyncApp,
3143    ) -> Result<proto::GetRemotesResponse> {
3144        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3145        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3146
3147        let branch_name = envelope.payload.branch_name;
3148        let is_push = envelope.payload.is_push;
3149
3150        let remotes = repository_handle
3151            .update(&mut cx, |repository_handle, _| {
3152                repository_handle.get_remotes(branch_name, is_push)
3153            })
3154            .await??;
3155        let remote_urls = repository_handle
3156            .update(&mut cx, |repository_handle, _| {
3157                repository_handle.remote_urls()
3158            })
3159            .await??;
3160
3161        Ok(proto::GetRemotesResponse {
3162            remotes: remotes
3163                .into_iter()
3164                .map(|remotes| proto::get_remotes_response::Remote {
3165                    name: remotes.name.to_string(),
3166                    url: remote_urls.get(remotes.name.as_ref()).cloned(),
3167                })
3168                .collect::<Vec<_>>(),
3169        })
3170    }
3171
3172    async fn handle_get_worktrees(
3173        this: Entity<Self>,
3174        envelope: TypedEnvelope<proto::GitGetWorktrees>,
3175        mut cx: AsyncApp,
3176    ) -> Result<proto::GitWorktreesResponse> {
3177        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3178        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3179
3180        let worktrees = repository_handle
3181            .update(&mut cx, |repository_handle, _| {
3182                repository_handle.worktrees()
3183            })
3184            .await??;
3185
3186        Ok(proto::GitWorktreesResponse {
3187            worktrees: worktrees
3188                .into_iter()
3189                .map(|worktree| worktree_to_proto(&worktree))
3190                .collect::<Vec<_>>(),
3191        })
3192    }
3193
3194    async fn handle_create_worktree(
3195        this: Entity<Self>,
3196        envelope: TypedEnvelope<proto::GitCreateWorktree>,
3197        mut cx: AsyncApp,
3198    ) -> Result<proto::Ack> {
3199        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3200        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3201        let directory = PathBuf::from(envelope.payload.directory);
3202        let name = envelope.payload.name;
3203        let commit = envelope.payload.commit;
3204        let use_existing_branch = envelope.payload.use_existing_branch;
3205        let target = if name.is_empty() {
3206            CreateWorktreeTarget::Detached { base_sha: commit }
3207        } else if use_existing_branch {
3208            CreateWorktreeTarget::ExistingBranch { branch_name: name }
3209        } else {
3210            CreateWorktreeTarget::NewBranch {
3211                branch_name: name,
3212                base_sha: commit,
3213            }
3214        };
3215
3216        repository_handle
3217            .update(&mut cx, |repository_handle, _| {
3218                repository_handle.create_worktree(target, directory)
3219            })
3220            .await??;
3221
3222        Ok(proto::Ack {})
3223    }
3224
3225    async fn handle_remove_worktree(
3226        this: Entity<Self>,
3227        envelope: TypedEnvelope<proto::GitRemoveWorktree>,
3228        mut cx: AsyncApp,
3229    ) -> Result<proto::Ack> {
3230        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3231        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3232        let path = PathBuf::from(envelope.payload.path);
3233        let force = envelope.payload.force;
3234
3235        repository_handle
3236            .update(&mut cx, |repository_handle, _| {
3237                repository_handle.remove_worktree(path, force)
3238            })
3239            .await??;
3240
3241        Ok(proto::Ack {})
3242    }
3243
3244    async fn handle_rename_worktree(
3245        this: Entity<Self>,
3246        envelope: TypedEnvelope<proto::GitRenameWorktree>,
3247        mut cx: AsyncApp,
3248    ) -> Result<proto::Ack> {
3249        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3250        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3251        let old_path = PathBuf::from(envelope.payload.old_path);
3252        let new_path = PathBuf::from(envelope.payload.new_path);
3253
3254        repository_handle
3255            .update(&mut cx, |repository_handle, _| {
3256                repository_handle.rename_worktree(old_path, new_path)
3257            })
3258            .await??;
3259
3260        Ok(proto::Ack {})
3261    }
3262
3263    async fn handle_worktree_created_at(
3264        this: Entity<Self>,
3265        envelope: TypedEnvelope<proto::GitWorktreeCreatedAt>,
3266        mut cx: AsyncApp,
3267    ) -> Result<proto::GitWorktreeCreatedAtResponse> {
3268        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3269        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3270        let worktree_path = PathBuf::from(envelope.payload.worktree_path);
3271
3272        let created_at = repository_handle
3273            .update(&mut cx, |repository_handle, _| {
3274                repository_handle.worktree_created_at(worktree_path)
3275            })
3276            .await??;
3277
3278        Ok(proto::GitWorktreeCreatedAtResponse {
3279            created_at: created_at.map(Into::into),
3280        })
3281    }
3282
3283    async fn handle_get_head_sha(
3284        this: Entity<Self>,
3285        envelope: TypedEnvelope<proto::GitGetHeadSha>,
3286        mut cx: AsyncApp,
3287    ) -> Result<proto::GitGetHeadShaResponse> {
3288        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3289        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3290
3291        let head_sha = repository_handle
3292            .update(&mut cx, |repository_handle, _| repository_handle.head_sha())
3293            .await??;
3294
3295        Ok(proto::GitGetHeadShaResponse { sha: head_sha })
3296    }
3297
3298    async fn handle_get_commit_data(
3299        this: Entity<Self>,
3300        envelope: TypedEnvelope<proto::GetCommitData>,
3301        mut cx: AsyncApp,
3302    ) -> Result<proto::GetCommitDataResponse> {
3303        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3304        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3305
3306        let shas: Vec<Oid> = envelope
3307            .payload
3308            .shas
3309            .iter()
3310            .filter_map(|s| Oid::from_str(s).ok())
3311            .collect();
3312
3313        let mut commits = Vec::with_capacity(shas.len());
3314        let mut receivers = Vec::new();
3315
3316        repository_handle.update(&mut cx, |repository, cx| {
3317            for &sha in &shas {
3318                match repository.fetch_commit_data(sha, true, cx) {
3319                    CommitDataState::Loaded(data) => {
3320                        commits.push(commit_data_to_proto(data));
3321                    }
3322                    CommitDataState::Loading(Some(shared)) => {
3323                        receivers.push(shared.clone());
3324                    }
3325                    CommitDataState::Loading(None) => {
3326                        // todo(git_graph) this could happen if the request fails, we should encode an error case
3327                        debug_panic!(
3328                            "This should never happen since we passed true into fetch commit data"
3329                        );
3330                    }
3331                }
3332            }
3333        });
3334
3335        let results = future::join_all(receivers).await;
3336
3337        commits.extend(
3338            results
3339                .into_iter()
3340                .filter_map(|result| result.ok())
3341                .map(|data| commit_data_to_proto(&data)),
3342        );
3343
3344        Ok(proto::GetCommitDataResponse { commits })
3345    }
3346
3347    async fn handle_get_initial_graph_data(
3348        this: Entity<Self>,
3349        envelope: TypedEnvelope<proto::GetInitialGraphData>,
3350        mut cx: AsyncApp,
3351    ) -> Result<impl Stream<Item = Result<proto::GetInitialGraphDataResponse>>> {
3352        const CHUNK_SIZE: usize = git::repository::GRAPH_CHUNK_SIZE;
3353        let payload = envelope.payload;
3354
3355        let repository_id = RepositoryId::from_proto(payload.repository_id);
3356        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3357
3358        let log_order = log_order_from_proto(payload.log_order());
3359        let log_source = log_source_from_proto(
3360            payload
3361                .log_source
3362                .context("missing initial graph data log source")?,
3363        )?;
3364
3365        let (subscriber_sender, subscriber_receiver) = async_channel::unbounded();
3366        let (cached_commits, error, is_loading) =
3367            repository_handle.update(&mut cx, |repository, cx| {
3368                let response =
3369                    repository.graph_data(log_source.clone(), log_order, 0..usize::MAX, cx);
3370                let cached_commits = response.commits.to_vec();
3371                let error = response.error.clone();
3372                let is_loading = response.is_loading;
3373
3374                if is_loading {
3375                    if let Some(graph_data) = repository
3376                        .initial_graph_data
3377                        .get_mut(&(log_source.clone(), log_order))
3378                    {
3379                        graph_data.subscribers.push(subscriber_sender);
3380                    }
3381                }
3382
3383                (cached_commits, error, is_loading)
3384            });
3385
3386        let (mut response_tx, response_rx) = mpsc::unbounded();
3387        cx.background_spawn(async move {
3388            if let Some(error) = error {
3389                if response_tx
3390                    .send(Err(anyhow!(error.to_string())))
3391                    .await
3392                    .is_err()
3393                {
3394                    return;
3395                }
3396                return;
3397            }
3398
3399            for commits in cached_commits.chunks(CHUNK_SIZE) {
3400                let response = proto::GetInitialGraphDataResponse {
3401                    commits: commits
3402                        .iter()
3403                        .map(|commit| initial_graph_commit_to_proto(commit))
3404                        .collect(),
3405                };
3406                if response_tx.send(Ok(response)).await.is_err() {
3407                    return;
3408                }
3409            }
3410
3411            if !is_loading {
3412                return;
3413            }
3414
3415            while let Ok(chunk_result) = subscriber_receiver.recv().await {
3416                let commits = match chunk_result {
3417                    Ok(commits) => commits,
3418                    Err(error) => {
3419                        response_tx
3420                            .send(Err(anyhow!(error.to_string())))
3421                            .await
3422                            .context("Failed to send error")
3423                            .log_err();
3424                        return;
3425                    }
3426                };
3427
3428                for commits in commits.chunks(CHUNK_SIZE) {
3429                    let response = proto::GetInitialGraphDataResponse {
3430                        commits: commits
3431                            .iter()
3432                            .map(|commit| initial_graph_commit_to_proto(commit))
3433                            .collect(),
3434                    };
3435                    if response_tx.send(Ok(response)).await.is_err() {
3436                        return;
3437                    }
3438                }
3439            }
3440        })
3441        .detach();
3442
3443        Ok(response_rx)
3444    }
3445
3446    async fn handle_search_commits(
3447        this: Entity<Self>,
3448        envelope: TypedEnvelope<proto::SearchCommits>,
3449        mut cx: AsyncApp,
3450    ) -> Result<impl Stream<Item = Result<proto::SearchCommitsResponse>>> {
3451        const CHUNK_SIZE: usize = 100;
3452
3453        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3454        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3455        let log_source = log_source_from_proto(
3456            envelope
3457                .payload
3458                .log_source
3459                .context("missing search commit log source")?,
3460        )?;
3461        let search_args = SearchCommitArgs {
3462            query: SharedString::from(envelope.payload.query),
3463            case_sensitive: envelope.payload.case_sensitive,
3464        };
3465
3466        let (request_tx, request_rx) = async_channel::unbounded();
3467        repository_handle.update(&mut cx, |repository, cx| {
3468            repository.search_commits(log_source, search_args, request_tx, cx);
3469        });
3470
3471        let (mut response_tx, response_rx) = mpsc::unbounded();
3472        cx.background_spawn(async move {
3473            let mut shas = Vec::new();
3474
3475            while let Ok(sha) = request_rx.recv().await {
3476                shas.push(sha.to_string());
3477
3478                if shas.len() >= CHUNK_SIZE {
3479                    if response_tx
3480                        .send(Ok(proto::SearchCommitsResponse {
3481                            shas: mem::take(&mut shas),
3482                        }))
3483                        .await
3484                        .is_err()
3485                    {
3486                        return;
3487                    }
3488                }
3489            }
3490
3491            if !shas.is_empty() {
3492                response_tx
3493                    .send(Ok(proto::SearchCommitsResponse { shas }))
3494                    .await
3495                    .ok();
3496            }
3497        })
3498        .detach();
3499
3500        Ok(response_rx)
3501    }
3502
3503    async fn handle_edit_ref(
3504        this: Entity<Self>,
3505        envelope: TypedEnvelope<proto::GitEditRef>,
3506        mut cx: AsyncApp,
3507    ) -> Result<proto::Ack> {
3508        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3509        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3510        let ref_name = envelope.payload.ref_name;
3511        let commit = match envelope.payload.action {
3512            Some(proto::git_edit_ref::Action::UpdateToCommit(sha)) => Some(sha),
3513            Some(proto::git_edit_ref::Action::Delete(_)) => None,
3514            None => anyhow::bail!("GitEditRef missing action"),
3515        };
3516
3517        repository_handle
3518            .update(&mut cx, |repository_handle, _| {
3519                repository_handle.edit_ref(ref_name, commit)
3520            })
3521            .await??;
3522
3523        Ok(proto::Ack {})
3524    }
3525
3526    async fn handle_repair_worktrees(
3527        this: Entity<Self>,
3528        envelope: TypedEnvelope<proto::GitRepairWorktrees>,
3529        mut cx: AsyncApp,
3530    ) -> Result<proto::Ack> {
3531        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3532        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3533
3534        repository_handle
3535            .update(&mut cx, |repository_handle, _| {
3536                repository_handle.repair_worktrees()
3537            })
3538            .await??;
3539
3540        Ok(proto::Ack {})
3541    }
3542
3543    async fn handle_get_branches(
3544        this: Entity<Self>,
3545        envelope: TypedEnvelope<proto::GitGetBranches>,
3546        mut cx: AsyncApp,
3547    ) -> Result<proto::GitBranchesResponse> {
3548        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3549        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3550
3551        let branches_scan = repository_handle
3552            .update(&mut cx, |repository_handle, _| repository_handle.branches())
3553            .await??;
3554
3555        Ok(proto::GitBranchesResponse {
3556            branches: branches_scan
3557                .branches
3558                .into_iter()
3559                .map(|branch| branch_to_proto(&branch))
3560                .collect::<Vec<_>>(),
3561            error: branches_scan.error.map(|error| error.to_string()),
3562        })
3563    }
3564    async fn handle_get_default_branch(
3565        this: Entity<Self>,
3566        envelope: TypedEnvelope<proto::GetDefaultBranch>,
3567        mut cx: AsyncApp,
3568    ) -> Result<proto::GetDefaultBranchResponse> {
3569        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3570        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3571
3572        let branch = repository_handle
3573            .update(&mut cx, |repository_handle, _| {
3574                repository_handle.default_branch(envelope.payload.include_remote_name)
3575            })
3576            .await??
3577            .map(Into::into);
3578
3579        Ok(proto::GetDefaultBranchResponse { branch })
3580    }
3581    async fn handle_create_branch(
3582        this: Entity<Self>,
3583        envelope: TypedEnvelope<proto::GitCreateBranch>,
3584        mut cx: AsyncApp,
3585    ) -> Result<proto::Ack> {
3586        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3587        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3588        let branch_name = envelope.payload.branch_name;
3589        let base_branch = envelope.payload.base_branch;
3590
3591        repository_handle
3592            .update(&mut cx, |repository_handle, _| {
3593                repository_handle.create_branch(branch_name, base_branch)
3594            })
3595            .await??;
3596
3597        Ok(proto::Ack {})
3598    }
3599
3600    async fn handle_change_branch(
3601        this: Entity<Self>,
3602        envelope: TypedEnvelope<proto::GitChangeBranch>,
3603        mut cx: AsyncApp,
3604    ) -> Result<proto::Ack> {
3605        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3606        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3607        let branch_name = envelope.payload.branch_name;
3608
3609        repository_handle
3610            .update(&mut cx, |repository_handle, _| {
3611                repository_handle.change_branch(branch_name)
3612            })
3613            .await??;
3614
3615        Ok(proto::Ack {})
3616    }
3617
3618    async fn handle_rename_branch(
3619        this: Entity<Self>,
3620        envelope: TypedEnvelope<proto::GitRenameBranch>,
3621        mut cx: AsyncApp,
3622    ) -> Result<proto::Ack> {
3623        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3624        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3625        let branch = envelope.payload.branch;
3626        let new_name = envelope.payload.new_name;
3627
3628        repository_handle
3629            .update(&mut cx, |repository_handle, _| {
3630                repository_handle.rename_branch(branch, new_name)
3631            })
3632            .await??;
3633
3634        Ok(proto::Ack {})
3635    }
3636
3637    async fn handle_create_remote(
3638        this: Entity<Self>,
3639        envelope: TypedEnvelope<proto::GitCreateRemote>,
3640        mut cx: AsyncApp,
3641    ) -> Result<proto::Ack> {
3642        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3643        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3644        let remote_name = envelope.payload.remote_name;
3645        let remote_url = envelope.payload.remote_url;
3646
3647        repository_handle
3648            .update(&mut cx, |repository_handle, _| {
3649                repository_handle.create_remote(remote_name, remote_url)
3650            })
3651            .await??;
3652
3653        Ok(proto::Ack {})
3654    }
3655
3656    async fn handle_delete_branch(
3657        this: Entity<Self>,
3658        envelope: TypedEnvelope<proto::GitDeleteBranch>,
3659        mut cx: AsyncApp,
3660    ) -> Result<proto::Ack> {
3661        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3662        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3663        let is_remote = envelope.payload.is_remote;
3664        let branch_name = envelope.payload.branch_name;
3665        let force = envelope.payload.force;
3666
3667        repository_handle
3668            .update(&mut cx, |repository_handle, _| {
3669                repository_handle.delete_branch(is_remote, branch_name, force)
3670            })
3671            .await??;
3672
3673        Ok(proto::Ack {})
3674    }
3675
3676    async fn handle_remove_remote(
3677        this: Entity<Self>,
3678        envelope: TypedEnvelope<proto::GitRemoveRemote>,
3679        mut cx: AsyncApp,
3680    ) -> Result<proto::Ack> {
3681        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3682        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3683        let remote_name = envelope.payload.remote_name;
3684
3685        repository_handle
3686            .update(&mut cx, |repository_handle, _| {
3687                repository_handle.remove_remote(remote_name)
3688            })
3689            .await??;
3690
3691        Ok(proto::Ack {})
3692    }
3693
3694    async fn handle_show(
3695        this: Entity<Self>,
3696        envelope: TypedEnvelope<proto::GitShow>,
3697        mut cx: AsyncApp,
3698    ) -> Result<proto::GitCommitDetails> {
3699        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3700        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3701
3702        let commit = repository_handle
3703            .update(&mut cx, |repository_handle, _| {
3704                repository_handle.show(envelope.payload.commit)
3705            })
3706            .await??;
3707        Ok(proto::GitCommitDetails {
3708            sha: commit.sha.into(),
3709            message: commit.message.into(),
3710            commit_timestamp: commit.commit_timestamp,
3711            author_email: commit.author_email.into(),
3712            author_name: commit.author_name.into(),
3713        })
3714    }
3715
3716    async fn handle_create_checkpoint(
3717        this: Entity<Self>,
3718        envelope: TypedEnvelope<proto::GitCreateCheckpoint>,
3719        mut cx: AsyncApp,
3720    ) -> Result<proto::GitCreateCheckpointResponse> {
3721        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3722        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3723
3724        let checkpoint = repository_handle
3725            .update(&mut cx, |repository, _| repository.checkpoint())
3726            .await??;
3727
3728        Ok(proto::GitCreateCheckpointResponse {
3729            commit_sha: checkpoint.commit_sha.as_bytes().to_vec(),
3730        })
3731    }
3732
3733    async fn handle_create_archive_checkpoint(
3734        this: Entity<Self>,
3735        envelope: TypedEnvelope<proto::GitCreateArchiveCheckpoint>,
3736        mut cx: AsyncApp,
3737    ) -> Result<proto::GitCreateArchiveCheckpointResponse> {
3738        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3739        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3740
3741        let (staged_commit_sha, unstaged_commit_sha) = repository_handle
3742            .update(&mut cx, |repository, _| {
3743                repository.create_archive_checkpoint()
3744            })
3745            .await??;
3746
3747        Ok(proto::GitCreateArchiveCheckpointResponse {
3748            staged_commit_sha,
3749            unstaged_commit_sha,
3750        })
3751    }
3752
3753    async fn handle_restore_checkpoint(
3754        this: Entity<Self>,
3755        envelope: TypedEnvelope<proto::GitRestoreCheckpoint>,
3756        mut cx: AsyncApp,
3757    ) -> Result<proto::Ack> {
3758        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3759        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3760
3761        let checkpoint = GitRepositoryCheckpoint {
3762            commit_sha: Oid::from_bytes(&envelope.payload.commit_sha)?,
3763        };
3764
3765        repository_handle
3766            .update(&mut cx, |repository, _| {
3767                repository.restore_checkpoint(checkpoint)
3768            })
3769            .await??;
3770
3771        Ok(proto::Ack {})
3772    }
3773
3774    async fn handle_restore_archive_checkpoint(
3775        this: Entity<Self>,
3776        envelope: TypedEnvelope<proto::GitRestoreArchiveCheckpoint>,
3777        mut cx: AsyncApp,
3778    ) -> Result<proto::Ack> {
3779        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3780        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3781        let staged_commit_sha = envelope.payload.staged_commit_sha;
3782        let unstaged_commit_sha = envelope.payload.unstaged_commit_sha;
3783
3784        repository_handle
3785            .update(&mut cx, |repository, _| {
3786                repository.restore_archive_checkpoint(staged_commit_sha, unstaged_commit_sha)
3787            })
3788            .await??;
3789
3790        Ok(proto::Ack {})
3791    }
3792
3793    async fn handle_compare_checkpoints(
3794        this: Entity<Self>,
3795        envelope: TypedEnvelope<proto::GitCompareCheckpoints>,
3796        mut cx: AsyncApp,
3797    ) -> Result<proto::GitCompareCheckpointsResponse> {
3798        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3799        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3800
3801        let left = GitRepositoryCheckpoint {
3802            commit_sha: Oid::from_bytes(&envelope.payload.left_commit_sha)?,
3803        };
3804        let right = GitRepositoryCheckpoint {
3805            commit_sha: Oid::from_bytes(&envelope.payload.right_commit_sha)?,
3806        };
3807
3808        let equal = repository_handle
3809            .update(&mut cx, |repository, _| {
3810                repository.compare_checkpoints(left, right)
3811            })
3812            .await??;
3813
3814        Ok(proto::GitCompareCheckpointsResponse { equal })
3815    }
3816
3817    async fn handle_diff_checkpoints(
3818        this: Entity<Self>,
3819        envelope: TypedEnvelope<proto::GitDiffCheckpoints>,
3820        mut cx: AsyncApp,
3821    ) -> Result<proto::GitDiffCheckpointsResponse> {
3822        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3823        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3824
3825        let base = GitRepositoryCheckpoint {
3826            commit_sha: Oid::from_bytes(&envelope.payload.base_commit_sha)?,
3827        };
3828        let target = GitRepositoryCheckpoint {
3829            commit_sha: Oid::from_bytes(&envelope.payload.target_commit_sha)?,
3830        };
3831
3832        let diff = repository_handle
3833            .update(&mut cx, |repository, _| {
3834                repository.diff_checkpoints(base, target)
3835            })
3836            .await??;
3837
3838        Ok(proto::GitDiffCheckpointsResponse { diff })
3839    }
3840
3841    async fn handle_load_commit_diff(
3842        this: Entity<Self>,
3843        envelope: TypedEnvelope<proto::LoadCommitDiff>,
3844        mut cx: AsyncApp,
3845    ) -> Result<proto::LoadCommitDiffResponse> {
3846        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3847        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3848
3849        let commit_diff = repository_handle
3850            .update(&mut cx, |repository_handle, _| {
3851                repository_handle.load_commit_diff(envelope.payload.commit)
3852            })
3853            .await??;
3854        Ok(proto::LoadCommitDiffResponse {
3855            files: commit_diff
3856                .files
3857                .into_iter()
3858                .map(|file| proto::CommitFile {
3859                    path: file.path.as_unix_str().to_owned(),
3860                    old_text: file.old_text,
3861                    new_text: file.new_text,
3862                    is_binary: file.is_binary,
3863                })
3864                .collect(),
3865        })
3866    }
3867
3868    async fn handle_reset(
3869        this: Entity<Self>,
3870        envelope: TypedEnvelope<proto::GitReset>,
3871        mut cx: AsyncApp,
3872    ) -> Result<proto::Ack> {
3873        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3874        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3875
3876        let mode = match envelope.payload.mode() {
3877            git_reset::ResetMode::Soft => ResetMode::Soft,
3878            git_reset::ResetMode::Mixed => ResetMode::Mixed,
3879        };
3880
3881        repository_handle
3882            .update(&mut cx, |repository_handle, cx| {
3883                repository_handle.reset(envelope.payload.commit, mode, cx)
3884            })
3885            .await??;
3886        Ok(proto::Ack {})
3887    }
3888
3889    async fn handle_checkout_files(
3890        this: Entity<Self>,
3891        envelope: TypedEnvelope<proto::GitCheckoutFiles>,
3892        mut cx: AsyncApp,
3893    ) -> Result<proto::Ack> {
3894        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3895        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3896        let paths = envelope
3897            .payload
3898            .paths
3899            .iter()
3900            .map(|s| RepoPath::from_proto(s))
3901            .collect::<Result<Vec<_>>>()?;
3902
3903        repository_handle
3904            .update(&mut cx, |repository_handle, cx| {
3905                repository_handle.checkout_files(&envelope.payload.commit, paths, cx)
3906            })
3907            .await?;
3908        Ok(proto::Ack {})
3909    }
3910
3911    async fn handle_add_path_to_gitignore(
3912        this: Entity<Self>,
3913        envelope: TypedEnvelope<proto::GitAddPathToGitignore>,
3914        mut cx: AsyncApp,
3915    ) -> Result<proto::Ack> {
3916        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3917        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3918        let repo_path = RepoPath::from_proto(&envelope.payload.path)?;
3919
3920        repository_handle
3921            .update(&mut cx, |repository_handle, _| {
3922                repository_handle.add_path_to_gitignore(&repo_path, envelope.payload.is_dir)
3923            })
3924            .await??;
3925        Ok(proto::Ack {})
3926    }
3927
3928    async fn handle_add_path_to_git_info_exclude(
3929        this: Entity<Self>,
3930        envelope: TypedEnvelope<proto::GitAddPathToGitInfoExclude>,
3931        mut cx: AsyncApp,
3932    ) -> Result<proto::Ack> {
3933        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3934        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
3935        let repo_path = RepoPath::from_proto(&envelope.payload.path)?;
3936
3937        repository_handle
3938            .update(&mut cx, |repository_handle, _| {
3939                repository_handle.add_path_to_git_info_exclude(&repo_path, envelope.payload.is_dir)
3940            })
3941            .await??;
3942        Ok(proto::Ack {})
3943    }
3944
3945    async fn handle_open_commit_message_buffer(
3946        this: Entity<Self>,
3947        envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
3948        mut cx: AsyncApp,
3949    ) -> Result<proto::OpenBufferResponse> {
3950        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3951        let repository = Self::repository_for_request(&this, repository_id, &mut cx)?;
3952        let buffer = repository
3953            .update(&mut cx, |repository, cx| {
3954                repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
3955            })
3956            .await?;
3957
3958        let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id());
3959        this.update(&mut cx, |this, cx| {
3960            this.buffer_store.update(cx, |buffer_store, cx| {
3961                buffer_store
3962                    .create_buffer_for_peer(
3963                        &buffer,
3964                        envelope.original_sender_id.unwrap_or(envelope.sender_id),
3965                        cx,
3966                    )
3967                    .detach_and_log_err(cx);
3968            })
3969        });
3970
3971        Ok(proto::OpenBufferResponse {
3972            buffer_id: buffer_id.to_proto(),
3973        })
3974    }
3975
3976    async fn handle_askpass(
3977        this: Entity<Self>,
3978        envelope: TypedEnvelope<proto::AskPassRequest>,
3979        mut cx: AsyncApp,
3980    ) -> Result<proto::AskPassResponse> {
3981        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
3982        let repository = Self::repository_for_request(&this, repository_id, &mut cx)?;
3983
3984        let delegates = cx.update(|cx| repository.read(cx).askpass_delegates.clone());
3985        let Some(mut askpass) = delegates.lock().remove(&envelope.payload.askpass_id) else {
3986            debug_panic!("no askpass found");
3987            anyhow::bail!("no askpass found");
3988        };
3989
3990        let response = askpass
3991            .ask_password(envelope.payload.prompt)
3992            .await
3993            .ok_or_else(|| anyhow::anyhow!("askpass cancelled"))?;
3994
3995        delegates
3996            .lock()
3997            .insert(envelope.payload.askpass_id, askpass);
3998
3999        // In fact, we don't quite know what we're doing here, as we're sending askpass password unencrypted, but..
4000        Ok(proto::AskPassResponse {
4001            response: response.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)?,
4002        })
4003    }
4004
4005    async fn handle_check_for_pushed_commits(
4006        this: Entity<Self>,
4007        envelope: TypedEnvelope<proto::CheckForPushedCommits>,
4008        mut cx: AsyncApp,
4009    ) -> Result<proto::CheckForPushedCommitsResponse> {
4010        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
4011        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
4012
4013        let branches = repository_handle
4014            .update(&mut cx, |repository_handle, _| {
4015                repository_handle.check_for_pushed_commits()
4016            })
4017            .await??;
4018        Ok(proto::CheckForPushedCommitsResponse {
4019            pushed_to: branches
4020                .into_iter()
4021                .map(|commit| commit.to_string())
4022                .collect(),
4023        })
4024    }
4025
4026    async fn handle_git_diff(
4027        this: Entity<Self>,
4028        envelope: TypedEnvelope<proto::GitDiff>,
4029        mut cx: AsyncApp,
4030    ) -> Result<proto::GitDiffResponse> {
4031        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
4032        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
4033        let diff_type = match envelope.payload.diff_type() {
4034            proto::git_diff::DiffType::HeadToIndex => DiffType::HeadToIndex,
4035            proto::git_diff::DiffType::HeadToWorktree => DiffType::HeadToWorktree,
4036            proto::git_diff::DiffType::MergeBase => {
4037                let base_ref = envelope
4038                    .payload
4039                    .merge_base_ref
4040                    .ok_or_else(|| anyhow!("merge_base_ref is required for MergeBase diff type"))?;
4041                DiffType::MergeBase {
4042                    base_ref: base_ref.into(),
4043                }
4044            }
4045        };
4046
4047        let mut diff = repository_handle
4048            .update(&mut cx, |repository_handle, cx| {
4049                repository_handle.diff(diff_type, cx)
4050            })
4051            .await??;
4052        const ONE_MB: usize = 1_000_000;
4053        if diff.len() > ONE_MB {
4054            diff = diff.chars().take(ONE_MB).collect()
4055        }
4056
4057        Ok(proto::GitDiffResponse { diff })
4058    }
4059
4060    async fn handle_tree_diff(
4061        this: Entity<Self>,
4062        request: TypedEnvelope<proto::GetTreeDiff>,
4063        mut cx: AsyncApp,
4064    ) -> Result<proto::GetTreeDiffResponse> {
4065        let repository_id = RepositoryId(request.payload.repository_id);
4066        let diff_type = if request.payload.is_merge {
4067            DiffTreeType::MergeBase {
4068                base: request.payload.base.into(),
4069                head: request.payload.head.into(),
4070            }
4071        } else {
4072            DiffTreeType::Since {
4073                base: request.payload.base.into(),
4074                head: request.payload.head.into(),
4075            }
4076        };
4077
4078        let diff = this
4079            .update(&mut cx, |this, cx| {
4080                let repository = this.repositories().get(&repository_id)?;
4081                Some(repository.update(cx, |repo, cx| repo.diff_tree(diff_type, cx)))
4082            })
4083            .context("missing repository")?
4084            .await??;
4085
4086        Ok(proto::GetTreeDiffResponse {
4087            entries: diff
4088                .entries
4089                .into_iter()
4090                .map(|(path, status)| proto::TreeDiffStatus {
4091                    path: path.as_ref().as_unix_str().to_owned(),
4092                    status: match status {
4093                        TreeDiffStatus::Added {} => proto::tree_diff_status::Status::Added.into(),
4094                        TreeDiffStatus::Modified { .. } => {
4095                            proto::tree_diff_status::Status::Modified.into()
4096                        }
4097                        TreeDiffStatus::Deleted { .. } => {
4098                            proto::tree_diff_status::Status::Deleted.into()
4099                        }
4100                    },
4101                    oid: match status {
4102                        TreeDiffStatus::Deleted { old } | TreeDiffStatus::Modified { old } => {
4103                            Some(old.to_string())
4104                        }
4105                        TreeDiffStatus::Added => None,
4106                    },
4107                })
4108                .collect(),
4109        })
4110    }
4111
4112    async fn handle_get_blob_content(
4113        this: Entity<Self>,
4114        request: TypedEnvelope<proto::GetBlobContent>,
4115        mut cx: AsyncApp,
4116    ) -> Result<proto::GetBlobContentResponse> {
4117        let oid = git::Oid::from_str(&request.payload.oid)?;
4118        let repository_id = RepositoryId(request.payload.repository_id);
4119        let content = this
4120            .update(&mut cx, |this, cx| {
4121                let repository = this.repositories().get(&repository_id)?;
4122                Some(repository.update(cx, |repo, cx| repo.load_blob_content(oid, cx)))
4123            })
4124            .context("missing repository")?
4125            .await?;
4126        Ok(proto::GetBlobContentResponse { content })
4127    }
4128
4129    async fn handle_load_commit_template(
4130        this: Entity<Self>,
4131        request: TypedEnvelope<proto::LoadCommitTemplate>,
4132        mut cx: AsyncApp,
4133    ) -> Result<proto::LoadCommitTemplateResponse> {
4134        let repository_id = RepositoryId(request.payload.repository_id);
4135        let rx = this
4136            .update(&mut cx, |this, cx| {
4137                let repository = this.repositories().get(&repository_id)?;
4138                Some(repository.update(cx, |repo, _| repo.load_commit_template_text()))
4139            })
4140            .context("missing repository")?;
4141        let template = rx.await??;
4142        Ok(proto::LoadCommitTemplateResponse {
4143            template: template.map(|t| t.template),
4144        })
4145    }
4146
4147    async fn handle_open_unstaged_diff(
4148        this: Entity<Self>,
4149        request: TypedEnvelope<proto::OpenUnstagedDiff>,
4150        mut cx: AsyncApp,
4151    ) -> Result<proto::OpenUnstagedDiffResponse> {
4152        let buffer_id = BufferId::new(request.payload.buffer_id)?;
4153        let diff = this
4154            .update(&mut cx, |this, cx| {
4155                let buffer = this.buffer_store.read(cx).get(buffer_id)?;
4156                Some(this.open_unstaged_diff(buffer, cx))
4157            })
4158            .context("missing buffer")?
4159            .await?;
4160        this.update(&mut cx, |this, _| {
4161            let shared_diffs = this
4162                .shared_diffs
4163                .entry(request.original_sender_id.unwrap_or(request.sender_id))
4164                .or_default();
4165            shared_diffs.entry(buffer_id).or_default().unstaged = Some(diff.clone());
4166        });
4167        let staged_text = diff.read_with(&cx, |diff, cx| diff.base_text_string(cx));
4168        Ok(proto::OpenUnstagedDiffResponse { staged_text })
4169    }
4170
4171    async fn handle_open_uncommitted_diff(
4172        this: Entity<Self>,
4173        request: TypedEnvelope<proto::OpenUncommittedDiff>,
4174        mut cx: AsyncApp,
4175    ) -> Result<proto::OpenUncommittedDiffResponse> {
4176        let buffer_id = BufferId::new(request.payload.buffer_id)?;
4177        let diff = this
4178            .update(&mut cx, |this, cx| {
4179                let buffer = this.buffer_store.read(cx).get(buffer_id)?;
4180                Some(this.open_uncommitted_diff(buffer, cx))
4181            })
4182            .context("missing buffer")?
4183            .await?;
4184        this.update(&mut cx, |this, _| {
4185            let shared_diffs = this
4186                .shared_diffs
4187                .entry(request.original_sender_id.unwrap_or(request.sender_id))
4188                .or_default();
4189            shared_diffs.entry(buffer_id).or_default().uncommitted = Some(diff.clone());
4190        });
4191        this.read_with(&cx, |this, cx| {
4192            use proto::open_uncommitted_diff_response::Mode;
4193
4194            let diff_state = this.diffs.get(&buffer_id).context("missing diff state")?;
4195            let diff_state = diff_state.read(cx);
4196            let index_matches_head = diff_state.index_matches_head();
4197            let index_text = diff_state.index_text.clone();
4198            let head_text = diff_state.head_text.clone();
4199
4200            let response = if index_matches_head {
4201                proto::OpenUncommittedDiffResponse {
4202                    committed_text: head_text.map(|head| head.to_string()),
4203                    staged_text: None,
4204                    mode: Mode::IndexMatchesHead.into(),
4205                }
4206            } else {
4207                proto::OpenUncommittedDiffResponse {
4208                    committed_text: head_text.map(|head| head.to_string()),
4209                    staged_text: index_text.map(|index| index.to_string()),
4210                    mode: Mode::IndexAndHead.into(),
4211                }
4212            };
4213            anyhow::Ok(response)
4214        })
4215    }
4216
4217    async fn handle_update_diff_bases(
4218        this: Entity<Self>,
4219        request: TypedEnvelope<proto::UpdateDiffBases>,
4220        mut cx: AsyncApp,
4221    ) -> Result<()> {
4222        let buffer_id = BufferId::new(request.payload.buffer_id)?;
4223        this.update(&mut cx, |this, cx| {
4224            if let Some(diff_state) = this.diffs.get_mut(&buffer_id)
4225                && let Some(buffer) = this.buffer_store.read(cx).get(buffer_id)
4226            {
4227                let buffer = buffer.read(cx).text_snapshot();
4228                diff_state.update(cx, |diff_state, cx| {
4229                    diff_state.handle_base_texts_updated(buffer, request.payload, cx);
4230                })
4231            }
4232        });
4233        Ok(())
4234    }
4235
4236    async fn handle_blame_buffer(
4237        this: Entity<Self>,
4238        envelope: TypedEnvelope<proto::BlameBuffer>,
4239        mut cx: AsyncApp,
4240    ) -> Result<proto::BlameBufferResponse> {
4241        let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
4242        let version = deserialize_version(&envelope.payload.version);
4243        let buffer = this.read_with(&cx, |this, cx| {
4244            this.buffer_store.read(cx).get_existing(buffer_id)
4245        })?;
4246        buffer
4247            .update(&mut cx, |buffer, _| {
4248                buffer.wait_for_version(version.clone())
4249            })
4250            .await?;
4251        let blame = this
4252            .update(&mut cx, |this, cx| {
4253                this.blame_buffer(&buffer, Some(version), cx)
4254            })
4255            .await?;
4256        Ok(serialize_blame_buffer_response(blame))
4257    }
4258
4259    async fn handle_get_permalink_to_line(
4260        this: Entity<Self>,
4261        envelope: TypedEnvelope<proto::GetPermalinkToLine>,
4262        mut cx: AsyncApp,
4263    ) -> Result<proto::GetPermalinkToLineResponse> {
4264        let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
4265        // let version = deserialize_version(&envelope.payload.version);
4266        let selection = {
4267            let proto_selection = envelope
4268                .payload
4269                .selection
4270                .context("no selection to get permalink for defined")?;
4271            proto_selection.start as u32..proto_selection.end as u32
4272        };
4273        let buffer = this.read_with(&cx, |this, cx| {
4274            this.buffer_store.read(cx).get_existing(buffer_id)
4275        })?;
4276        let permalink = this
4277            .update(&mut cx, |this, cx| {
4278                this.get_permalink_to_line(&buffer, selection, cx)
4279            })
4280            .await?;
4281        Ok(proto::GetPermalinkToLineResponse {
4282            permalink: permalink.to_string(),
4283        })
4284    }
4285
4286    fn repository_for_request(
4287        this: &Entity<Self>,
4288        id: RepositoryId,
4289        cx: &mut AsyncApp,
4290    ) -> Result<Entity<Repository>> {
4291        this.read_with(cx, |this, _| {
4292            this.repositories
4293                .get(&id)
4294                .context("missing repository handle")
4295                .cloned()
4296        })
4297    }
4298
4299    pub fn repo_snapshots(&self, cx: &App) -> HashMap<RepositoryId, RepositorySnapshot> {
4300        self.repositories
4301            .iter()
4302            .map(|(id, repo)| (*id, repo.read(cx).snapshot.clone()))
4303            .collect()
4304    }
4305
4306    fn coalesce_repo_paths(mut paths: Vec<RepoPath>) -> Vec<RepoPath> {
4307        paths.sort();
4308
4309        let mut coalesced = Vec::with_capacity(paths.len());
4310        for path in paths {
4311            if coalesced
4312                .last()
4313                .is_some_and(|ancestor: &RepoPath| path.starts_with(ancestor))
4314            {
4315                continue;
4316            }
4317            coalesced.push(path);
4318        }
4319
4320        coalesced
4321    }
4322
4323    fn process_updated_entries(
4324        &self,
4325        worktree: &Entity<Worktree>,
4326        updated_entries: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
4327        cx: &mut App,
4328    ) -> Task<HashMap<Entity<Repository>, Vec<RepoPath>>> {
4329        let path_style = worktree.read(cx).path_style();
4330        let mut repo_paths = self
4331            .repositories
4332            .values()
4333            .map(|repo| (repo.read(cx).work_directory_abs_path.clone(), repo.clone()))
4334            .collect::<Vec<_>>();
4335        let mut entries: Vec<_> = updated_entries
4336            .iter()
4337            .map(|(path, _, _)| path.clone())
4338            .collect();
4339        entries.sort();
4340        let worktree = worktree.read(cx);
4341
4342        let entries = entries
4343            .into_iter()
4344            .map(|path| worktree.absolutize(&path))
4345            .collect::<Arc<[_]>>();
4346
4347        let executor = cx.background_executor().clone();
4348        cx.background_executor().spawn(async move {
4349            repo_paths.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0));
4350            let mut paths_by_git_repo = HashMap::<_, Vec<_>>::default();
4351            let mut tasks = FuturesOrdered::new();
4352            for (repo_path, repo) in repo_paths.into_iter().rev() {
4353                let entries = entries.clone();
4354                let task = executor.spawn(async move {
4355                    // Find all repository paths that belong to this repo
4356                    let mut ix = entries.partition_point(|path| path < &*repo_path);
4357                    if ix == entries.len() {
4358                        return None;
4359                    };
4360
4361                    let mut paths = Vec::new();
4362                    // All paths prefixed by a given repo will constitute a continuous range.
4363                    while let Some(path) = entries.get(ix)
4364                        && let Some(repo_path) = RepositorySnapshot::abs_path_to_repo_path_inner(
4365                            &repo_path, path, path_style,
4366                        )
4367                    {
4368                        paths.push((repo_path, ix));
4369                        ix += 1;
4370                    }
4371                    if paths.is_empty() {
4372                        None
4373                    } else {
4374                        Some((repo, paths))
4375                    }
4376                });
4377                tasks.push_back(task);
4378            }
4379
4380            // Now, let's filter out the "duplicate" entries that were processed by multiple distinct repos.
4381            let mut path_was_used = vec![false; entries.len()];
4382            let tasks = tasks.collect::<Vec<_>>().await;
4383            // Process tasks from the back: iterating backwards allows us to see more-specific paths first.
4384            // We always want to assign a path to it's innermost repository.
4385            for t in tasks {
4386                let Some((repo, paths)) = t else {
4387                    continue;
4388                };
4389                let entry = paths_by_git_repo.entry(repo).or_default();
4390                for (repo_path, ix) in paths {
4391                    if path_was_used[ix] {
4392                        continue;
4393                    }
4394                    path_was_used[ix] = true;
4395                    entry.push(repo_path);
4396                }
4397            }
4398
4399            for paths in paths_by_git_repo.values_mut() {
4400                *paths = Self::coalesce_repo_paths(mem::take(paths));
4401            }
4402
4403            paths_by_git_repo
4404        })
4405    }
4406}
4407
4408impl BufferGitState {
4409    fn new(_git_store: WeakEntity<GitStore>, _cx: &mut Context<Self>) -> Self {
4410        Self {
4411            unstaged_diff: Default::default(),
4412            staged_diff: Default::default(),
4413            uncommitted_diff: Default::default(),
4414            oid_diffs: Default::default(),
4415            recalculate_diff_task: Default::default(),
4416            language: Default::default(),
4417            language_registry: Default::default(),
4418            recalculating_tx: postage::watch::channel_with(false).0,
4419            hunk_staging_operation_count: 0,
4420            hunk_staging_operation_count_as_of_write: 0,
4421            head_text: Default::default(),
4422            index_text: Default::default(),
4423            pending_index_edits: Some(Vec::new()),
4424            oid_texts: Default::default(),
4425            head_text_buffer: WeakEntity::new_invalid(),
4426            index_text_buffer: WeakEntity::new_invalid(),
4427            index_text_buffer_language_enabled: Default::default(),
4428            head_changed: Default::default(),
4429            index_changed: Default::default(),
4430            language_changed: Default::default(),
4431            conflict_updated_futures: Default::default(),
4432            conflict_set: Default::default(),
4433            reparse_conflict_markers_task: Default::default(),
4434        }
4435    }
4436
4437    fn get_or_create_head_text_buffer(&mut self, cx: &mut Context<Self>) -> Entity<Buffer> {
4438        if let Some(buffer) = self.head_text_buffer.upgrade() {
4439            return buffer;
4440        }
4441        let head_text = self.head_text.clone();
4442        let buffer = cx.new(|cx| {
4443            let mut buffer = Buffer::local(head_text.as_deref().unwrap_or(""), cx);
4444            buffer.set_capability(Capability::ReadOnly, cx);
4445            buffer
4446        });
4447        self.head_text_buffer = buffer.downgrade();
4448        buffer
4449    }
4450
4451    fn index_text_buffer(&self) -> Option<Entity<Buffer>> {
4452        self.index_text_buffer.upgrade()
4453    }
4454
4455    fn get_or_create_index_text_buffer(
4456        &mut self,
4457        file: Option<Arc<dyn language::File>>,
4458        cx: &mut Context<Self>,
4459    ) -> Entity<Buffer> {
4460        if let Some(buffer) = self.index_text_buffer.upgrade() {
4461            if let Some(file) = file {
4462                buffer.update(cx, |buffer, cx| buffer.file_updated(file, cx));
4463            }
4464            return buffer;
4465        }
4466        let index_text = self.index_text.clone();
4467        let buffer = cx.new(|cx| {
4468            let mut buffer = Buffer::local(index_text.as_deref().unwrap_or(""), cx);
4469            if let Some(file) = file {
4470                buffer.file_updated(file, cx);
4471            }
4472            buffer.set_capability(Capability::ReadOnly, cx);
4473            buffer
4474        });
4475        self.index_text_buffer = buffer.downgrade();
4476        buffer
4477    }
4478
4479    #[ztracing::instrument(skip_all)]
4480    fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
4481        self.language = buffer.read(cx).language().cloned();
4482        self.language_changed = true;
4483        let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
4484    }
4485
4486    fn reparse_conflict_markers(
4487        &mut self,
4488        buffer: text::BufferSnapshot,
4489        cx: &mut Context<Self>,
4490    ) -> oneshot::Receiver<()> {
4491        let (tx, rx) = oneshot::channel();
4492
4493        let Some(conflict_set) = self
4494            .conflict_set
4495            .as_ref()
4496            .and_then(|conflict_set| conflict_set.upgrade())
4497        else {
4498            return rx;
4499        };
4500
4501        let has_conflict = conflict_set.read_with(cx, |conflict_set, _| conflict_set.has_conflict);
4502        if !has_conflict {
4503            return rx;
4504        }
4505
4506        let old_snapshot = conflict_set.read_with(cx, |conflict_set, _| conflict_set.snapshot());
4507        self.conflict_updated_futures.push(tx);
4508        self.reparse_conflict_markers_task = Some(cx.spawn(async move |this, cx| {
4509            let (snapshot, changed_range) = cx
4510                .background_spawn(async move {
4511                    let new_snapshot = ConflictSet::parse(&buffer);
4512                    let changed_range = old_snapshot.compare(&new_snapshot, &buffer);
4513                    (new_snapshot, changed_range)
4514                })
4515                .await;
4516            this.update(cx, |this, cx| {
4517                if let Some(conflict_set) = &this.conflict_set {
4518                    conflict_set
4519                        .update(cx, |conflict_set, cx| {
4520                            conflict_set.set_snapshot(snapshot, changed_range, cx);
4521                        })
4522                        .ok();
4523                }
4524                let futures = std::mem::take(&mut this.conflict_updated_futures);
4525                for tx in futures {
4526                    tx.send(()).ok();
4527                }
4528            })
4529        }));
4530
4531        rx
4532    }
4533
4534    fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
4535        self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
4536    }
4537
4538    fn staged_diff(&self) -> Option<Entity<BufferDiff>> {
4539        self.staged_diff.as_ref().and_then(|(set, _)| set.upgrade())
4540    }
4541
4542    fn staged_diff_and_index_text_buffer(&self) -> Option<(Entity<BufferDiff>, Entity<Buffer>)> {
4543        let (diff, index_text_buffer) = self.staged_diff.as_ref()?;
4544        Some((diff.upgrade()?, index_text_buffer.clone()))
4545    }
4546
4547    fn uncommitted_diff(&self) -> Option<Entity<BufferDiff>> {
4548        self.uncommitted_diff.as_ref().and_then(|set| set.upgrade())
4549    }
4550
4551    fn oid_diff(&self, oid: Option<git::Oid>) -> Option<Entity<BufferDiff>> {
4552        self.oid_diffs.get(&oid).and_then(|weak| weak.upgrade())
4553    }
4554
4555    /// Whether the index text is known to match the committed text, without
4556    /// comparing their contents. Always true when both texts were set by a
4557    /// single `DiffBasesChange::SetBoth`, which shares one allocation between
4558    /// them. May be false even when the contents are equal, if the texts were
4559    /// loaded separately.
4560    fn index_matches_head(&self) -> bool {
4561        match (self.index_text.as_ref(), self.head_text.as_ref()) {
4562            (Some(index), Some(head)) => Arc::ptr_eq(index, head),
4563            (None, None) => true,
4564            _ => false,
4565        }
4566    }
4567
4568    fn remove_overlapping_pending_index_edits(&mut self, ranges: &[Range<usize>]) {
4569        if let Some(edits) = &mut self.pending_index_edits {
4570            edits.retain(|(existing, _)| {
4571                ranges.iter().all(|footprint| {
4572                    existing.end < footprint.start || footprint.end < existing.start
4573                })
4574            });
4575        }
4576    }
4577
4578    fn insert_pending_index_edits(&mut self, edits: Option<Vec<(Range<usize>, Arc<str>)>>) {
4579        match edits {
4580            None => {
4581                self.pending_index_edits = None;
4582            }
4583            Some(new_edits) => {
4584                let mut edits = self.pending_index_edits.take().unwrap_or_default();
4585                for (range, replacement) in new_edits {
4586                    edits.retain(|(existing, _)| {
4587                        existing.end < range.start || range.end < existing.start
4588                    });
4589                    let position =
4590                        edits.partition_point(|(existing, _)| existing.start < range.start);
4591                    edits.insert(position, (range, replacement));
4592                }
4593                self.pending_index_edits = Some(edits);
4594            }
4595        }
4596    }
4597
4598    fn pending_index_text(&self, cx: &App) -> Option<Rope> {
4599        let index_text_buffer = self.index_text_buffer.upgrade()?;
4600        let edits = self.pending_index_edits.as_ref()?;
4601        #[cfg(debug_assertions)]
4602        for window in edits.windows(2) {
4603            debug_assert!(window[0].0.end <= window[1].0.start);
4604        }
4605        let mut index_text = index_text_buffer.read(cx).text_snapshot().as_rope().clone();
4606        for (old_range, replacement_text) in edits.iter().rev() {
4607            index_text.replace(old_range.clone(), replacement_text);
4608        }
4609        Some(index_text)
4610    }
4611
4612    fn clear_pending_index_edits(&mut self) {
4613        self.pending_index_edits = Some(Vec::new());
4614    }
4615
4616    fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
4617        for diff in [
4618            self.uncommitted_diff(),
4619            self.unstaged_diff(),
4620            self.staged_diff(),
4621        ]
4622        .into_iter()
4623        .flatten()
4624        {
4625            diff.update(cx, |diff, cx| diff.clear_pending_hunks(cx));
4626        }
4627    }
4628
4629    fn mark_whole_file_stage_or_unstage_pending(
4630        &mut self,
4631        stage: bool,
4632        buffer_snapshot: &text::BufferSnapshot,
4633        cx: &mut Context<Self>,
4634    ) {
4635        if let Some(uncommitted_diff) = self.uncommitted_diff() {
4636            uncommitted_diff.update(cx, |uncommitted_diff, cx| {
4637                uncommitted_diff.mark_all_hunks_pending(stage, buffer_snapshot, cx);
4638            });
4639        }
4640
4641        if stage {
4642            if let Some(unstaged_diff) = self.unstaged_diff() {
4643                unstaged_diff.update(cx, |unstaged_diff, cx| {
4644                    unstaged_diff.suppress_all_hunks_pending(buffer_snapshot, cx);
4645                });
4646            }
4647        } else if let Some(staged_diff) = self.staged_diff()
4648            && let Some(index_text_buffer) = self.index_text_buffer()
4649        {
4650            let index_snapshot = index_text_buffer.read(cx).text_snapshot();
4651            staged_diff.update(cx, |staged_diff, cx| {
4652                staged_diff.suppress_all_hunks_pending(&index_snapshot, cx);
4653            });
4654        }
4655    }
4656
4657    fn clear_pending_index_edits_and_hunks(&mut self, cx: &mut Context<Self>) {
4658        self.clear_pending_index_edits();
4659        self.clear_pending_hunks(cx);
4660    }
4661
4662    fn handle_base_texts_updated(
4663        &mut self,
4664        buffer: text::BufferSnapshot,
4665        message: proto::UpdateDiffBases,
4666        cx: &mut Context<Self>,
4667    ) {
4668        use proto::update_diff_bases::Mode;
4669
4670        let Some(mode) = Mode::from_i32(message.mode) else {
4671            return;
4672        };
4673
4674        let diff_bases_change = match mode {
4675            Mode::HeadOnly => Some(DiffBasesChange::SetHead(message.committed_text)),
4676            Mode::IndexOnly => Some(DiffBasesChange::SetIndex(message.staged_text)),
4677            Mode::IndexMatchesHead => Some(DiffBasesChange::SetBoth(message.committed_text)),
4678            Mode::IndexAndHead => Some(DiffBasesChange::SetEach {
4679                index: message.staged_text,
4680                head: message.committed_text,
4681            }),
4682            Mode::Unchanged => None,
4683        };
4684
4685        self.diff_bases_changed(buffer, diff_bases_change, cx);
4686    }
4687
4688    pub fn wait_for_recalculation(&mut self) -> Option<impl Future<Output = ()> + use<>> {
4689        if *self.recalculating_tx.borrow() {
4690            let mut rx = self.recalculating_tx.subscribe();
4691            Some(async move {
4692                loop {
4693                    let is_recalculating = rx.recv().await;
4694                    if is_recalculating != Some(true) {
4695                        break;
4696                    }
4697                }
4698            })
4699        } else {
4700            None
4701        }
4702    }
4703
4704    fn diff_bases_changed(
4705        &mut self,
4706        buffer: text::BufferSnapshot,
4707        diff_bases_change: Option<DiffBasesChange>,
4708        cx: &mut Context<Self>,
4709    ) {
4710        match diff_bases_change {
4711            Some(DiffBasesChange::SetIndex(index)) => {
4712                self.index_text = index.map(|mut index| {
4713                    text::LineEnding::normalize(&mut index);
4714                    Arc::from(index.as_str())
4715                });
4716                self.index_changed = true;
4717            }
4718            Some(DiffBasesChange::SetHead(head)) => {
4719                self.head_text = head.map(|mut head| {
4720                    text::LineEnding::normalize(&mut head);
4721                    Arc::from(head.as_str())
4722                });
4723                self.head_changed = true;
4724            }
4725            Some(DiffBasesChange::SetBoth(text)) => {
4726                let text = text.map(|mut text| {
4727                    text::LineEnding::normalize(&mut text);
4728                    Arc::from(text.as_str())
4729                });
4730                self.head_text = text.clone();
4731                self.index_text = text;
4732                self.head_changed = true;
4733                self.index_changed = true;
4734            }
4735            Some(DiffBasesChange::SetEach { index, head }) => {
4736                self.index_text = index.map(|mut index| {
4737                    text::LineEnding::normalize(&mut index);
4738                    Arc::from(index.as_str())
4739                });
4740                self.index_changed = true;
4741                self.head_text = head.map(|mut head| {
4742                    text::LineEnding::normalize(&mut head);
4743                    Arc::from(head.as_str())
4744                });
4745                self.head_changed = true;
4746            }
4747            None => {}
4748        }
4749
4750        self.recalculate_diffs(buffer, cx)
4751    }
4752
4753    #[ztracing::instrument(skip_all)]
4754    fn recalculate_diffs(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
4755        *self.recalculating_tx.borrow_mut() = true;
4756
4757        let language = self.language.clone();
4758        let language_registry = self.language_registry.clone();
4759        let unstaged_diff = self.unstaged_diff();
4760        let staged_diff = self.staged_diff();
4761        let uncommitted_diff = self.uncommitted_diff();
4762        let head = self.head_text.clone();
4763        let index = self.index_text.clone();
4764        let head_text_buffer = self.head_text_buffer.upgrade();
4765        let index_text_buffer = self.index_text_buffer.upgrade();
4766        let index_text_buffer_language_enabled = self.index_text_buffer_language_enabled;
4767        let index_changed = self.index_changed;
4768        let head_changed = self.head_changed;
4769        let language_changed = self.language_changed;
4770        let prev_hunk_staging_operation_count = self.hunk_staging_operation_count_as_of_write;
4771        let index_matches_head = self.index_matches_head();
4772
4773        let oid_diffs: Vec<(
4774            Option<git::Oid>,
4775            Entity<BufferDiff>,
4776            Entity<Buffer>,
4777            Option<Arc<str>>,
4778        )> = self
4779            .oid_diffs
4780            .iter()
4781            .filter_map(|(oid, weak)| {
4782                let diff = weak.upgrade()?;
4783                let base_text_buffer = diff.read(cx).base_text_buffer().clone();
4784                let base_text = match oid {
4785                    Some(oid) => Some(self.oid_texts.get(oid)?.clone()),
4786                    None => None,
4787                };
4788                Some((*oid, diff, base_text_buffer, base_text))
4789            })
4790            .collect();
4791
4792        self.oid_diffs.retain(|oid, weak| {
4793            let alive = weak.upgrade().is_some();
4794            if !alive {
4795                if let Some(oid) = oid {
4796                    self.oid_texts.remove(oid);
4797                }
4798            }
4799            alive
4800        });
4801        if self
4802            .staged_diff
4803            .as_ref()
4804            .is_some_and(|(weak, _)| !weak.is_upgradable())
4805        {
4806            self.staged_diff = None;
4807        }
4808        self.recalculate_diff_task = Some(cx.spawn(async move |this, cx| {
4809            log::debug!(
4810                "start recalculating diffs for buffer {}",
4811                buffer.remote_id()
4812            );
4813
4814            if index_text_buffer_language_enabled
4815                && let Some(index_text_buffer) = &index_text_buffer
4816            {
4817                index_text_buffer.update(cx, |index_text_buffer, cx| {
4818                    if let Some(language_registry) = language_registry.clone() {
4819                        index_text_buffer.set_language_registry(language_registry);
4820                    }
4821                    index_text_buffer.set_language_async(language.clone(), cx);
4822                });
4823            }
4824            if let Some(head_text_buffer) = &head_text_buffer {
4825                head_text_buffer.update(cx, |head_text_buffer, cx| {
4826                    if let Some(language_registry) = language_registry.clone() {
4827                        head_text_buffer.set_language_registry(language_registry);
4828                    }
4829                    head_text_buffer.set_language_async(language.clone(), cx);
4830                });
4831            }
4832
4833            for (_, _, base_text_buffer, _) in &oid_diffs {
4834                base_text_buffer.update(cx, |base_text_buffer, cx| {
4835                    if let Some(language_registry) = language_registry.clone() {
4836                        base_text_buffer.set_language_registry(language_registry);
4837                    }
4838                    base_text_buffer.set_language_async(language.clone(), cx);
4839                });
4840            }
4841
4842            let mut edited_index_text = None;
4843
4844            let index_text_snapshot = if let Some(index_text_buffer) = &index_text_buffer
4845                && (unstaged_diff.is_some() || staged_diff.is_some())
4846            {
4847                let index_text_snapshot = if index_changed || language_changed {
4848                    let new_index_text = index.clone().unwrap_or_default();
4849                    let index_text_diff = index_text_buffer
4850                        .update(cx, |index_text_buffer, cx| {
4851                            index_text_buffer.diff(new_index_text.clone(), cx)
4852                        })
4853                        .await;
4854                    let edited = index_text_buffer
4855                        .update(cx, |index_text_buffer, cx| {
4856                            index_text_buffer.snapshot_with_edits(index_text_diff.edits, cx)
4857                        })
4858                        .await;
4859                    let snapshot = edited.snapshot().clone();
4860                    edited_index_text = Some(edited);
4861                    snapshot
4862                } else {
4863                    index_text_buffer.read_with(cx, |buffer, _| buffer.snapshot())
4864                };
4865                Some(index_text_snapshot)
4866            } else {
4867                None
4868            };
4869
4870            let mut new_unstaged_diff = None;
4871
4872            if let (Some(unstaged_diff), Some(index_text_snapshot)) =
4873                (unstaged_diff.as_ref(), index_text_snapshot.as_ref())
4874            {
4875                new_unstaged_diff = Some(
4876                    cx.update(|cx| {
4877                        unstaged_diff.read(cx).update_diff(
4878                            buffer.clone(),
4879                            index_text_snapshot,
4880                            index.clone(),
4881                            cx,
4882                        )
4883                    })
4884                    .await,
4885                );
4886            }
4887
4888            // Dropping BufferDiff can be expensive, so yield back to the event loop
4889            // for a bit
4890            yield_now().await;
4891
4892            let mut edited_head_text = None;
4893            let mut new_staged_diff = None;
4894            let mut new_uncommitted_diff = None;
4895            if let Some(head_text_buffer) = &head_text_buffer
4896                && (staged_diff.is_some() || uncommitted_diff.is_some())
4897            {
4898                let head_base_text_exists = head.is_some();
4899                let head_text_snapshot = if head_changed || language_changed {
4900                    let new_head_text = head.clone().unwrap_or_default();
4901                    let head_text_diff = head_text_buffer
4902                        .update(cx, |head_text_buffer, cx| {
4903                            head_text_buffer.diff(new_head_text.clone(), cx)
4904                        })
4905                        .await;
4906                    let edited = head_text_buffer
4907                        .update(cx, |base_text_buffer, cx| {
4908                            base_text_buffer.snapshot_with_edits(head_text_diff.edits, cx)
4909                        })
4910                        .await;
4911                    let snapshot = edited.snapshot().clone();
4912                    edited_head_text = Some(edited);
4913                    snapshot
4914                } else {
4915                    head_text_buffer.read_with(cx, |buffer, _| buffer.snapshot())
4916                };
4917                if let (Some(staged_diff), Some(index_base_text_snapshot)) =
4918                    (staged_diff.as_ref(), index_text_snapshot.as_ref())
4919                {
4920                    new_staged_diff = Some(
4921                        cx.update(|cx| {
4922                            staged_diff.read(cx).update_diff(
4923                                index_base_text_snapshot.text.clone(),
4924                                &head_text_snapshot,
4925                                head.clone(),
4926                                cx,
4927                            )
4928                        })
4929                        .await,
4930                    );
4931                }
4932
4933                if let Some(uncommitted_diff) = &uncommitted_diff {
4934                    new_uncommitted_diff = if index_matches_head {
4935                        new_unstaged_diff.clone().map(|mut update| {
4936                            update.set_base_text_snapshot(
4937                                head_text_snapshot.clone(),
4938                                head_base_text_exists,
4939                            );
4940                            update
4941                        })
4942                    } else {
4943                        None
4944                    };
4945                    if new_uncommitted_diff.is_none() {
4946                        new_uncommitted_diff = Some(
4947                            cx.update(|cx| {
4948                                uncommitted_diff.read(cx).update_diff(
4949                                    buffer.clone(),
4950                                    &head_text_snapshot,
4951                                    head.clone(),
4952                                    cx,
4953                                )
4954                            })
4955                            .await,
4956                        );
4957                    }
4958                }
4959            }
4960
4961            // Dropping BufferDiff can be expensive, so yield back to the event loop
4962            // for a bit
4963            yield_now().await;
4964
4965            let cancel = this.update(cx, |this, _| {
4966                // This checks whether all pending stage/unstage operations
4967                // have quiesced (i.e. both the corresponding write and the
4968                // read of that write have completed). If not, then we cancel
4969                // this recalculation attempt to avoid invalidating pending
4970                // state too quickly; another recalculation will come along
4971                // later and clear the pending state once the state of the index has settled.
4972                if this.hunk_staging_operation_count > prev_hunk_staging_operation_count {
4973                    *this.recalculating_tx.borrow_mut() = false;
4974                    true
4975                } else {
4976                    false
4977                }
4978            })?;
4979            if cancel {
4980                log::debug!(
4981                    concat!(
4982                        "aborting recalculating diffs for buffer {}",
4983                        "due to subsequent hunk operations",
4984                    ),
4985                    buffer.remote_id()
4986                );
4987                return Ok(());
4988            }
4989
4990            this.update(cx, |this, cx| {
4991                this.clear_pending_index_edits();
4992
4993                if let (Some(staged_diff), Some(new_staged_diff)) =
4994                    (staged_diff.as_ref(), new_staged_diff.clone())
4995                {
4996                    staged_diff.update(cx, |diff, cx| {
4997                        if let Some(edited_base_text) = edited_index_text.take()
4998                            && let Some(index_text_buffer) = &index_text_buffer
4999                        {
5000                            index_text_buffer.update(cx, |index_text_buffer, cx| {
5001                                index_text_buffer.fast_forward(edited_base_text, cx)
5002                            });
5003                        }
5004                        if let Some(edited_head_text) = edited_head_text.take()
5005                            && let Some(head_text_buffer) = &head_text_buffer
5006                        {
5007                            head_text_buffer.update(cx, |head_text_buffer, cx| {
5008                                head_text_buffer.fast_forward(edited_head_text, cx)
5009                            });
5010                        }
5011                        diff.set_snapshot_with_secondary(new_staged_diff, None, true, cx)
5012                    });
5013                }
5014
5015                let unstaged_changed_range = if let (Some(unstaged_diff), Some(new_unstaged_diff)) =
5016                    (unstaged_diff.as_ref(), new_unstaged_diff.clone())
5017                {
5018                    Some(unstaged_diff.update(cx, |diff, cx| {
5019                        if let Some(edited_index_text) = edited_index_text.take()
5020                            && let Some(index_text_buffer) = &index_text_buffer
5021                        {
5022                            index_text_buffer.update(cx, |index_text_buffer, cx| {
5023                                index_text_buffer.fast_forward(edited_index_text, cx)
5024                            });
5025                        }
5026                        diff.set_snapshot_with_secondary(new_unstaged_diff, None, true, cx)
5027                    }))
5028                } else {
5029                    None
5030                };
5031
5032                if let (Some(uncommitted_diff), Some(new_uncommitted_diff)) =
5033                    (uncommitted_diff.as_ref(), new_uncommitted_diff.clone())
5034                {
5035                    uncommitted_diff.update(cx, |diff, cx| {
5036                        if let Some(edited_base_text) = edited_head_text.take()
5037                            && let Some(head_text_buffer) = &head_text_buffer
5038                        {
5039                            head_text_buffer.update(cx, |head_text_buffer, cx| {
5040                                head_text_buffer.fast_forward(edited_base_text, cx)
5041                            });
5042                        }
5043                        diff.set_snapshot_with_secondary(
5044                            new_uncommitted_diff,
5045                            unstaged_changed_range.flatten(),
5046                            true,
5047                            cx,
5048                        )
5049                    });
5050                }
5051            })?;
5052
5053            yield_now().await;
5054
5055            for (oid, oid_diff, base_text_buffer, base_text) in oid_diffs {
5056                let base_text_snapshot =
5057                    base_text_buffer.read_with(cx, |buffer, _| buffer.snapshot());
5058                let new_oid_diff = cx
5059                    .update(|cx| {
5060                        oid_diff.read(cx).update_diff(
5061                            buffer.clone(),
5062                            &base_text_snapshot,
5063                            base_text.clone(),
5064                            cx,
5065                        )
5066                    })
5067                    .await;
5068
5069                oid_diff.update(cx, |diff, cx| diff.set_snapshot(new_oid_diff, cx));
5070
5071                log::debug!(
5072                    "finished recalculating oid diff for buffer {} oid {:?}",
5073                    buffer.remote_id(),
5074                    oid
5075                );
5076
5077                yield_now().await;
5078            }
5079
5080            log::debug!(
5081                "finished recalculating diffs for buffer {}",
5082                buffer.remote_id()
5083            );
5084
5085            if let Some(this) = this.upgrade() {
5086                this.update(cx, |this, _| {
5087                    this.index_changed = false;
5088                    this.head_changed = false;
5089                    this.language_changed = false;
5090                    *this.recalculating_tx.borrow_mut() = false;
5091                });
5092            }
5093
5094            Ok(())
5095        }));
5096    }
5097}
5098
5099fn make_remote_delegate(
5100    this: Entity<GitStore>,
5101    project_id: u64,
5102    repository_id: RepositoryId,
5103    askpass_id: u64,
5104    cx: &mut AsyncApp,
5105) -> AskPassDelegate {
5106    AskPassDelegate::new(cx, move |prompt, tx, cx| {
5107        this.update(cx, |this, cx| {
5108            let Some((client, _)) = this.downstream_client() else {
5109                return;
5110            };
5111            let response = client.request(proto::AskPassRequest {
5112                project_id,
5113                repository_id: repository_id.to_proto(),
5114                askpass_id,
5115                prompt,
5116            });
5117            cx.spawn(async move |_, _| {
5118                let mut response = response.await?.response;
5119                tx.send(EncryptedPassword::try_from(response.as_ref())?)
5120                    .ok();
5121                response.zeroize();
5122                anyhow::Ok(())
5123            })
5124            .detach_and_log_err(cx);
5125        });
5126    })
5127}
5128
5129impl RepositoryId {
5130    pub fn to_proto(self) -> u64 {
5131        self.0
5132    }
5133
5134    pub fn from_proto(id: u64) -> Self {
5135        RepositoryId(id)
5136    }
5137}
5138
5139impl RepositorySnapshot {
5140    fn empty(
5141        id: RepositoryId,
5142        work_directory_abs_path: Arc<Path>,
5143        repository_dir_abs_path: Option<Arc<Path>>,
5144        dot_git_abs_path: Option<Arc<Path>>,
5145        common_dir_abs_path: Option<Arc<Path>>,
5146        path_style: PathStyle,
5147    ) -> Self {
5148        let repository_dir_abs_path =
5149            repository_dir_abs_path.unwrap_or_else(|| work_directory_abs_path.join(".git").into());
5150        let dot_git_abs_path =
5151            dot_git_abs_path.unwrap_or_else(|| work_directory_abs_path.join(".git").into());
5152        let common_dir_abs_path =
5153            common_dir_abs_path.unwrap_or_else(|| repository_dir_abs_path.clone());
5154
5155        Self {
5156            id,
5157            statuses_by_path: Default::default(),
5158            repository_dir_abs_path,
5159            dot_git_abs_path,
5160            common_dir_abs_path,
5161            work_directory_abs_path,
5162            branch: None,
5163            branch_list: Arc::from([]),
5164            branch_list_error: None,
5165            head_commit: None,
5166            scan_id: 0,
5167            merge: Default::default(),
5168            remote_origin_url: None,
5169            remote_upstream_url: None,
5170            stash_entries: Default::default(),
5171            linked_worktrees: Arc::from([]),
5172            path_style,
5173        }
5174    }
5175
5176    fn initial_update(&self, project_id: u64) -> proto::UpdateRepository {
5177        proto::UpdateRepository {
5178            branch_summary: self.branch.as_ref().map(branch_to_proto),
5179            branch_list: self.branch_list.iter().map(branch_to_proto).collect(),
5180            branch_list_error: self
5181                .branch_list_error
5182                .as_ref()
5183                .map(|error| error.to_string()),
5184            head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto),
5185            updated_statuses: self
5186                .statuses_by_path
5187                .iter()
5188                .map(|entry| entry.to_proto())
5189                .collect(),
5190            removed_statuses: Default::default(),
5191            current_merge_conflicts: self
5192                .merge
5193                .merge_heads_by_conflicted_path
5194                .iter()
5195                .map(|(repo_path, _)| repo_path.as_unix_str().to_owned())
5196                .collect(),
5197            merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
5198            project_id,
5199            id: self.id.to_proto(),
5200            abs_path: self.work_directory_abs_path.to_string_lossy().into_owned(),
5201            entry_ids: vec![self.id.to_proto()],
5202            scan_id: self.scan_id,
5203            is_last_update: true,
5204            stash_entries: self
5205                .stash_entries
5206                .entries
5207                .iter()
5208                .map(stash_to_proto)
5209                .collect(),
5210            remote_upstream_url: self.remote_upstream_url.clone(),
5211            remote_origin_url: self.remote_origin_url.clone(),
5212            repository_dir_abs_path: Some(
5213                self.repository_dir_abs_path.to_string_lossy().into_owned(),
5214            ),
5215            common_dir_abs_path: Some(self.common_dir_abs_path.to_string_lossy().into_owned()),
5216            linked_worktrees: self
5217                .linked_worktrees
5218                .iter()
5219                .map(worktree_to_proto)
5220                .collect(),
5221        }
5222    }
5223
5224    fn build_update(&self, old: &Self, project_id: u64) -> proto::UpdateRepository {
5225        let mut updated_statuses: Vec<proto::StatusEntry> = Vec::new();
5226        let mut removed_statuses: Vec<String> = Vec::new();
5227
5228        let mut new_statuses = self.statuses_by_path.iter().peekable();
5229        let mut old_statuses = old.statuses_by_path.iter().peekable();
5230
5231        let mut current_new_entry = new_statuses.next();
5232        let mut current_old_entry = old_statuses.next();
5233        loop {
5234            match (current_new_entry, current_old_entry) {
5235                (Some(new_entry), Some(old_entry)) => {
5236                    match new_entry.repo_path.cmp(&old_entry.repo_path) {
5237                        Ordering::Less => {
5238                            updated_statuses.push(new_entry.to_proto());
5239                            current_new_entry = new_statuses.next();
5240                        }
5241                        Ordering::Equal => {
5242                            if new_entry.status != old_entry.status
5243                                || new_entry.diff_stat != old_entry.diff_stat
5244                            {
5245                                updated_statuses.push(new_entry.to_proto());
5246                            }
5247                            current_old_entry = old_statuses.next();
5248                            current_new_entry = new_statuses.next();
5249                        }
5250                        Ordering::Greater => {
5251                            removed_statuses.push(old_entry.repo_path.as_unix_str().to_owned());
5252                            current_old_entry = old_statuses.next();
5253                        }
5254                    }
5255                }
5256                (None, Some(old_entry)) => {
5257                    removed_statuses.push(old_entry.repo_path.as_unix_str().to_owned());
5258                    current_old_entry = old_statuses.next();
5259                }
5260                (Some(new_entry), None) => {
5261                    updated_statuses.push(new_entry.to_proto());
5262                    current_new_entry = new_statuses.next();
5263                }
5264                (None, None) => break,
5265            }
5266        }
5267
5268        proto::UpdateRepository {
5269            branch_summary: self.branch.as_ref().map(branch_to_proto),
5270            branch_list: self.branch_list.iter().map(branch_to_proto).collect(),
5271            branch_list_error: self
5272                .branch_list_error
5273                .as_ref()
5274                .map(|error| error.to_string()),
5275            head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto),
5276            updated_statuses,
5277            removed_statuses,
5278            current_merge_conflicts: self
5279                .merge
5280                .merge_heads_by_conflicted_path
5281                .iter()
5282                .map(|(path, _)| path.as_unix_str().to_owned())
5283                .collect(),
5284            merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
5285            project_id,
5286            id: self.id.to_proto(),
5287            abs_path: self.work_directory_abs_path.to_string_lossy().into_owned(),
5288            entry_ids: vec![],
5289            scan_id: self.scan_id,
5290            is_last_update: true,
5291            stash_entries: self
5292                .stash_entries
5293                .entries
5294                .iter()
5295                .map(stash_to_proto)
5296                .collect(),
5297            remote_upstream_url: self.remote_upstream_url.clone(),
5298            remote_origin_url: self.remote_origin_url.clone(),
5299            repository_dir_abs_path: Some(
5300                self.repository_dir_abs_path.to_string_lossy().into_owned(),
5301            ),
5302            common_dir_abs_path: Some(self.common_dir_abs_path.to_string_lossy().into_owned()),
5303            linked_worktrees: self
5304                .linked_worktrees
5305                .iter()
5306                .map(worktree_to_proto)
5307                .collect(),
5308        }
5309    }
5310
5311    /// Returns the main worktree path for this repository, if one exists.
5312    ///
5313    /// Linked worktrees attached to bare repositories do not have a main
5314    /// worktree. For linked worktrees attached to a non-bare repository, the
5315    /// common Git directory is the main worktree's `.git` directory.
5316    pub fn main_worktree_abs_path(&self) -> Option<&Path> {
5317        if self.is_linked_worktree() {
5318            if self.common_dir_abs_path.file_name()? == std::ffi::OsStr::new(".git") {
5319                self.common_dir_abs_path.parent()
5320            } else {
5321                None
5322            }
5323        } else {
5324            Some(self.work_directory_abs_path.as_ref())
5325        }
5326    }
5327
5328    /// The main worktree is the original checkout that other worktrees were
5329    /// created from.
5330    ///
5331    /// For example, if you had both `~/code/zed` and `~/code/worktrees/zed-2`,
5332    /// then `~/code/zed` is the main worktree and `~/code/worktrees/zed-2` is a linked worktree.
5333    ///
5334    /// Submodules also return `true` here, since they are not linked worktrees.
5335    pub fn is_main_worktree(&self) -> bool {
5336        !self.is_linked_worktree()
5337    }
5338
5339    /// Returns true if this repository is a linked worktree, that is, one that
5340    /// was created from another worktree.
5341    ///
5342    /// Returns `false` for both the main worktree and submodules.
5343    pub fn is_linked_worktree(&self) -> bool {
5344        self.repository_dir_abs_path != self.common_dir_abs_path
5345    }
5346
5347    pub fn linked_worktrees(&self) -> &[GitWorktree] {
5348        &self.linked_worktrees
5349    }
5350
5351    pub fn status(&self) -> impl Iterator<Item = StatusEntry> + '_ {
5352        self.statuses_by_path.iter().cloned()
5353    }
5354
5355    pub fn status_summary(&self) -> GitSummary {
5356        self.statuses_by_path.summary().item_summary
5357    }
5358
5359    pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
5360        self.statuses_by_path
5361            .get(&PathKey(path.as_ref().clone()), ())
5362            .cloned()
5363    }
5364
5365    pub fn diff_stat_for_path(&self, path: &RepoPath) -> Option<DiffStat> {
5366        self.statuses_by_path
5367            .get(&PathKey(path.as_ref().clone()), ())
5368            .and_then(|entry| entry.diff_stat)
5369    }
5370
5371    pub fn abs_path_to_repo_path(&self, abs_path: &Path) -> Option<RepoPath> {
5372        Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path, self.path_style)
5373    }
5374
5375    fn repo_path_to_abs_path(&self, repo_path: &RepoPath) -> PathBuf {
5376        let repo_path = repo_path.display(self.path_style);
5377        PathBuf::from(
5378            self.path_style
5379                .join(&self.work_directory_abs_path, repo_path.as_ref())
5380                .unwrap(),
5381        )
5382    }
5383
5384    #[inline]
5385    fn abs_path_to_repo_path_inner(
5386        work_directory_abs_path: &Path,
5387        abs_path: &Path,
5388        path_style: PathStyle,
5389    ) -> Option<RepoPath> {
5390        let rel_path = path_style.strip_prefix(abs_path, work_directory_abs_path)?;
5391        Some(RepoPath::from_rel_path(&rel_path))
5392    }
5393
5394    pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool {
5395        self.merge
5396            .merge_heads_by_conflicted_path
5397            .contains_key(repo_path)
5398    }
5399
5400    pub fn has_conflict(&self, repo_path: &RepoPath) -> bool {
5401        let had_conflict_on_last_merge_head_change = self
5402            .merge
5403            .merge_heads_by_conflicted_path
5404            .contains_key(repo_path);
5405        let has_conflict_currently = self
5406            .status_for_path(repo_path)
5407            .is_some_and(|entry| entry.status.is_conflicted());
5408        had_conflict_on_last_merge_head_change || has_conflict_currently
5409    }
5410
5411    /// This is the name that will be displayed in the repository selector for this repository.
5412    pub fn display_name(&self) -> SharedString {
5413        self.work_directory_abs_path
5414            .file_name()
5415            .unwrap_or_default()
5416            .to_string_lossy()
5417            .to_string()
5418            .into()
5419    }
5420}
5421
5422pub fn stash_to_proto(entry: &StashEntry) -> proto::StashEntry {
5423    proto::StashEntry {
5424        oid: entry.oid.as_bytes().to_vec(),
5425        message: entry.message.clone(),
5426        branch: entry.branch.clone(),
5427        index: entry.index as u64,
5428        timestamp: entry.timestamp,
5429    }
5430}
5431
5432pub fn proto_to_stash(entry: &proto::StashEntry) -> Result<StashEntry> {
5433    Ok(StashEntry {
5434        oid: Oid::from_bytes(&entry.oid)?,
5435        message: entry.message.clone(),
5436        index: entry.index as usize,
5437        branch: entry.branch.clone(),
5438        timestamp: entry.timestamp,
5439    })
5440}
5441
5442impl MergeDetails {
5443    async fn update(
5444        &mut self,
5445        backend: &Arc<dyn GitRepository>,
5446        current_conflicted_paths: Vec<RepoPath>,
5447    ) -> bool {
5448        log::debug!("load merge details");
5449        self.message = backend.merge_message().await.map(SharedString::from);
5450        let heads = backend
5451            .revparse_batch(vec![
5452                "MERGE_HEAD".into(),
5453                "CHERRY_PICK_HEAD".into(),
5454                "REBASE_HEAD".into(),
5455                "REVERT_HEAD".into(),
5456                "APPLY_HEAD".into(),
5457            ])
5458            .await
5459            .log_err()
5460            .unwrap_or_default()
5461            .into_iter()
5462            .map(|opt| opt.map(SharedString::from))
5463            .collect::<Vec<_>>();
5464
5465        let mut conflicts_changed = false;
5466
5467        // Record the merge state for newly conflicted paths
5468        for path in &current_conflicted_paths {
5469            if self.merge_heads_by_conflicted_path.get(&path).is_none() {
5470                conflicts_changed = true;
5471                self.merge_heads_by_conflicted_path
5472                    .insert(path.clone(), heads.clone());
5473            }
5474        }
5475
5476        // Clear state for paths that are no longer conflicted and for which the merge heads have changed
5477        self.merge_heads_by_conflicted_path
5478            .retain(|path, old_merge_heads| {
5479                let keep = current_conflicted_paths.contains(path)
5480                    || (old_merge_heads == &heads
5481                        && old_merge_heads.iter().any(|head| head.is_some()));
5482                if !keep {
5483                    conflicts_changed = true;
5484                }
5485                keep
5486            });
5487
5488        conflicts_changed
5489    }
5490}
5491
5492impl Repository {
5493    pub fn is_trusted(&self) -> bool {
5494        match self.repository_state.peek() {
5495            Some(Ok(RepositoryState::Local(state))) => state.backend.is_trusted(),
5496            _ => false,
5497        }
5498    }
5499
5500    pub fn snapshot(&self) -> RepositorySnapshot {
5501        self.snapshot.clone()
5502    }
5503
5504    pub fn pending_ops(&self) -> impl Iterator<Item = PendingOps> + '_ {
5505        self.pending_ops.iter().cloned()
5506    }
5507
5508    pub fn pending_ops_summary(&self) -> PathSummary<PendingOpsSummary> {
5509        self.pending_ops.summary().clone()
5510    }
5511
5512    pub fn pending_ops_for_path(&self, path: &RepoPath) -> Option<PendingOps> {
5513        self.pending_ops
5514            .get(&PathKey(path.as_ref().clone()), ())
5515            .cloned()
5516    }
5517
5518    fn respawn_local_worker(
5519        &mut self,
5520        project_environment: WeakEntity<ProjectEnvironment>,
5521        fs: Arc<dyn Fs>,
5522        is_trusted: bool,
5523        cx: &mut Context<Self>,
5524    ) {
5525        let work_directory_abs_path = self.snapshot.work_directory_abs_path.clone();
5526        let dot_git_abs_path = self.snapshot.dot_git_abs_path.clone();
5527
5528        let state = cx
5529            .spawn(async move |_, cx| {
5530                LocalRepositoryState::new(
5531                    work_directory_abs_path,
5532                    dot_git_abs_path,
5533                    project_environment,
5534                    fs,
5535                    is_trusted,
5536                    cx,
5537                )
5538                .await
5539                .map_err(|err| err.to_string())
5540            })
5541            .shared();
5542        self.job_sender.close_channel();
5543        self._worker_task = Task::ready(());
5544        self.active_jobs.clear();
5545        self.job_debug_queue
5546            .mark_unfinished_complete(job_debug_queue::CompletedJobStatus::Skipped);
5547        cx.notify();
5548
5549        let (job_sender, worker_task) = Repository::spawn_local_git_worker(state.clone(), cx);
5550        self.job_sender = job_sender;
5551        self._worker_task = worker_task;
5552        self.repository_state = cx
5553            .spawn(async move |_, _| {
5554                let state = state.await?;
5555                Ok(RepositoryState::Local(state))
5556            })
5557            .shared();
5558    }
5559
5560    fn reinitialize_local_backend(
5561        &mut self,
5562        work_directory_abs_path: Arc<Path>,
5563        dot_git_abs_path: Arc<Path>,
5564        repository_dir_abs_path: Arc<Path>,
5565        common_dir_abs_path: Arc<Path>,
5566        project_environment: WeakEntity<ProjectEnvironment>,
5567        fs: Arc<dyn Fs>,
5568        is_trusted: bool,
5569        cx: &mut Context<Self>,
5570    ) {
5571        self.snapshot.work_directory_abs_path = work_directory_abs_path;
5572        self.snapshot.dot_git_abs_path = dot_git_abs_path;
5573        self.snapshot.repository_dir_abs_path = repository_dir_abs_path;
5574        self.snapshot.common_dir_abs_path = common_dir_abs_path;
5575        self.respawn_local_worker(project_environment, fs, is_trusted, cx);
5576    }
5577
5578    fn local(
5579        id: RepositoryId,
5580        work_directory_abs_path: Arc<Path>,
5581        repository_dir_abs_path: Arc<Path>,
5582        common_dir_abs_path: Arc<Path>,
5583        dot_git_abs_path: Arc<Path>,
5584        project_environment: WeakEntity<ProjectEnvironment>,
5585        fs: Arc<dyn Fs>,
5586        is_trusted: bool,
5587        git_store: WeakEntity<GitStore>,
5588        cx: &mut Context<Self>,
5589    ) -> Self {
5590        let snapshot = RepositorySnapshot::empty(
5591            id,
5592            work_directory_abs_path,
5593            Some(repository_dir_abs_path),
5594            Some(dot_git_abs_path),
5595            Some(common_dir_abs_path),
5596            PathStyle::local(),
5597        );
5598
5599        let mut repo = Repository {
5600            this: cx.weak_entity(),
5601            git_store,
5602            snapshot,
5603            pending_ops: Default::default(),
5604            repository_state: Task::ready(Err("not yet initialized".into())).shared(),
5605            _worker_task: Task::ready(()),
5606            commit_message_buffer: None,
5607            askpass_delegates: Default::default(),
5608            paths_needing_status_update: Default::default(),
5609            latest_askpass_id: 0,
5610            job_sender: mpsc::unbounded().0,
5611            job_id: 0,
5612            active_jobs: Default::default(),
5613            job_debug_queue: job_debug_queue::GitJobDebugQueue::new(),
5614            initial_graph_data: Default::default(),
5615            commit_data: Default::default(),
5616            commit_data_handler: CommitDataHandlerState::Closed,
5617        };
5618        repo.respawn_local_worker(project_environment, fs, is_trusted, cx);
5619        cx.subscribe_self(Self::handle_subscribe_self).detach();
5620        repo
5621    }
5622
5623    fn remote(
5624        id: RepositoryId,
5625        work_directory_abs_path: Arc<Path>,
5626        repository_dir_abs_path: Option<Arc<Path>>,
5627        common_dir_abs_path: Option<Arc<Path>>,
5628        path_style: PathStyle,
5629        project_id: ProjectId,
5630        client: AnyProtoClient,
5631        git_store: WeakEntity<GitStore>,
5632        cx: &mut Context<Self>,
5633    ) -> Self {
5634        let snapshot = RepositorySnapshot::empty(
5635            id,
5636            work_directory_abs_path,
5637            repository_dir_abs_path,
5638            None,
5639            common_dir_abs_path,
5640            path_style,
5641        );
5642
5643        let repository_state = RemoteRepositoryState { project_id, client };
5644        let (job_sender, worker_task) = Self::spawn_remote_git_worker(repository_state.clone(), cx);
5645        let repository_state = Task::ready(Ok(RepositoryState::Remote(repository_state))).shared();
5646        cx.subscribe_self(Self::handle_subscribe_self).detach();
5647
5648        Self {
5649            this: cx.weak_entity(),
5650            snapshot,
5651            commit_message_buffer: None,
5652            git_store,
5653            pending_ops: Default::default(),
5654            paths_needing_status_update: Default::default(),
5655            job_sender,
5656            _worker_task: worker_task,
5657            repository_state,
5658            askpass_delegates: Default::default(),
5659            latest_askpass_id: 0,
5660            active_jobs: Default::default(),
5661            job_debug_queue: job_debug_queue::GitJobDebugQueue::new(),
5662            job_id: 0,
5663            initial_graph_data: Default::default(),
5664            commit_data: Default::default(),
5665            commit_data_handler: CommitDataHandlerState::Closed,
5666        }
5667    }
5668
5669    fn handle_subscribe_self(&mut self, event: &RepositoryEvent, _: &mut Context<Self>) {
5670        // scan id greater than 2 means the initial snapshot was calculated,
5671        // otherwise we don't need to refresh the graph state
5672        match event {
5673            RepositoryEvent::HeadChanged | RepositoryEvent::BranchListChanged => {
5674                if self.scan_id > 2 {
5675                    self.initial_graph_data.clear();
5676                }
5677            }
5678            RepositoryEvent::StashEntriesChanged => {
5679                if self.scan_id > 2 {
5680                    self.initial_graph_data
5681                        .retain(|(log_source, _), _| *log_source != LogSource::All);
5682                }
5683            }
5684            _ => {}
5685        }
5686    }
5687
5688    pub fn git_store(&self) -> Option<Entity<GitStore>> {
5689        self.git_store.upgrade()
5690    }
5691
5692    fn reload_buffer_diff_bases(&mut self, cx: &mut Context<Self>) {
5693        let this = cx.weak_entity();
5694        let git_store = self.git_store.clone();
5695        let _ = self.send_keyed_job(
5696            "reload_buffer_diff_bases",
5697            Some(GitJobKey::ReloadBufferDiffBases),
5698            None,
5699            |state, mut cx| async move {
5700                let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else {
5701                    log::error!("tried to recompute diffs for a non-local repository");
5702                    return Ok(());
5703                };
5704
5705                let Some(this) = this.upgrade() else {
5706                    return Ok(());
5707                };
5708
5709                let repo_diff_state_updates = this.update(&mut cx, |this, cx| {
5710                    git_store.update(cx, |git_store, cx| {
5711                        git_store
5712                            .diffs
5713                            .iter()
5714                            .filter_map(|(buffer_id, diff_state)| {
5715                                let buffer_store = git_store.buffer_store.read(cx);
5716                                let buffer = buffer_store.get(*buffer_id)?;
5717                                let file = File::from_dyn(buffer.read(cx).file())?;
5718                                let abs_path = file.worktree.read(cx).absolutize(&file.path);
5719                                let repo_path = this.abs_path_to_repo_path(&abs_path)?;
5720                                let is_symlink = GitStore::file_is_symlink(file, cx);
5721                                log::debug!(
5722                                    "start reload diff bases for repo path {}",
5723                                    repo_path.as_unix_str()
5724                                );
5725                                diff_state.update(cx, |diff_state, _| {
5726                                    let has_unstaged_diff = diff_state
5727                                        .unstaged_diff
5728                                        .as_ref()
5729                                        .is_some_and(|diff| diff.is_upgradable());
5730                                    let has_staged_diff = diff_state
5731                                        .staged_diff
5732                                        .as_ref()
5733                                        .is_some_and(|(diff, _)| diff.is_upgradable());
5734                                    let has_uncommitted_diff = diff_state
5735                                        .uncommitted_diff
5736                                        .as_ref()
5737                                        .is_some_and(|set| set.is_upgradable());
5738
5739                                    Some((
5740                                        buffer,
5741                                        repo_path,
5742                                        is_symlink,
5743                                        (has_unstaged_diff || has_staged_diff)
5744                                            .then(|| diff_state.index_text.clone()),
5745                                        (has_staged_diff || has_uncommitted_diff)
5746                                            .then(|| diff_state.head_text.clone()),
5747                                    ))
5748                                })
5749                            })
5750                            .collect::<Vec<_>>()
5751                    })
5752                })?;
5753
5754                let buffer_diff_base_changes = cx
5755                    .background_spawn(async move {
5756                        let mut revisions = Vec::new();
5757                        for (_, repo_path, is_symlink, current_index_text, current_head_text) in
5758                            &repo_diff_state_updates
5759                        {
5760                            if current_index_text.is_some() && !*is_symlink {
5761                                revisions.push(format!(":{}", repo_path.as_unix_str()));
5762                            }
5763                            if current_head_text.is_some() && !*is_symlink {
5764                                revisions.push(format!("HEAD:{}", repo_path.as_unix_str()));
5765                            }
5766                        }
5767
5768                        let mut loaded_revisions = backend
5769                            .load_revisions(revisions)
5770                            .await
5771                            .log_err()
5772                            .into_iter()
5773                            .flatten();
5774
5775                        let mut changes = Vec::new();
5776                        for (buffer, _, is_symlink, current_index_text, current_head_text) in
5777                            &repo_diff_state_updates
5778                        {
5779                            let index_text = (current_index_text.is_some() && !*is_symlink)
5780                                .then(|| loaded_revisions.next().flatten())
5781                                .flatten();
5782
5783                            let head_text = (current_head_text.is_some() && !*is_symlink)
5784                                .then(|| loaded_revisions.next().flatten())
5785                                .flatten();
5786
5787                            let change =
5788                                match (current_index_text.as_ref(), current_head_text.as_ref()) {
5789                                    (Some(current_index), Some(current_head)) => {
5790                                        let index_changed =
5791                                            index_text.as_deref() != current_index.as_deref();
5792                                        let head_changed =
5793                                            head_text.as_deref() != current_head.as_deref();
5794                                        if index_changed && head_changed {
5795                                            if index_text == head_text {
5796                                                Some(DiffBasesChange::SetBoth(head_text))
5797                                            } else {
5798                                                Some(DiffBasesChange::SetEach {
5799                                                    index: index_text,
5800                                                    head: head_text,
5801                                                })
5802                                            }
5803                                        } else if index_changed {
5804                                            Some(DiffBasesChange::SetIndex(index_text))
5805                                        } else if head_changed {
5806                                            Some(DiffBasesChange::SetHead(head_text))
5807                                        } else {
5808                                            None
5809                                        }
5810                                    }
5811                                    (Some(current_index), None) => {
5812                                        let index_changed =
5813                                            index_text.as_deref() != current_index.as_deref();
5814                                        index_changed
5815                                            .then_some(DiffBasesChange::SetIndex(index_text))
5816                                    }
5817                                    (None, Some(current_head)) => {
5818                                        let head_changed =
5819                                            head_text.as_deref() != current_head.as_deref();
5820                                        head_changed.then_some(DiffBasesChange::SetHead(head_text))
5821                                    }
5822                                    (None, None) => None,
5823                                };
5824
5825                            changes.push((buffer.clone(), change))
5826                        }
5827                        changes
5828                    })
5829                    .await;
5830
5831                git_store.update(&mut cx, |git_store, cx| {
5832                    for (buffer, diff_bases_change) in buffer_diff_base_changes {
5833                        let buffer_snapshot = buffer.read(cx).text_snapshot();
5834                        let buffer_id = buffer_snapshot.remote_id();
5835                        let Some(diff_state) = git_store.diffs.get(&buffer_id) else {
5836                            continue;
5837                        };
5838
5839                        let downstream_client = git_store.downstream_client();
5840                        diff_state.update(cx, |diff_state, cx| {
5841                            use proto::update_diff_bases::Mode;
5842
5843                            if let Some((client, project_id)) = downstream_client {
5844                                let (staged_text, committed_text, mode) =
5845                                    match diff_bases_change.clone() {
5846                                        Some(DiffBasesChange::SetIndex(index)) => {
5847                                            (index, None, Mode::IndexOnly)
5848                                        }
5849                                        Some(DiffBasesChange::SetHead(head)) => {
5850                                            (None, head, Mode::HeadOnly)
5851                                        }
5852                                        Some(DiffBasesChange::SetEach { index, head }) => {
5853                                            (index, head, Mode::IndexAndHead)
5854                                        }
5855                                        Some(DiffBasesChange::SetBoth(text)) => {
5856                                            (None, text, Mode::IndexMatchesHead)
5857                                        }
5858                                        None => (None, None, Mode::Unchanged),
5859                                    };
5860                                client
5861                                    .send(proto::UpdateDiffBases {
5862                                        project_id: project_id.to_proto(),
5863                                        buffer_id: buffer_id.to_proto(),
5864                                        staged_text,
5865                                        committed_text,
5866                                        mode: mode as i32,
5867                                    })
5868                                    .log_err();
5869                            }
5870
5871                            diff_state.diff_bases_changed(buffer_snapshot, diff_bases_change, cx);
5872                        });
5873                    }
5874                })
5875            },
5876        );
5877    }
5878
5879    pub fn send_job<F, Fut, R>(
5880        &mut self,
5881        description: &'static str,
5882        status: Option<SharedString>,
5883        job: F,
5884    ) -> oneshot::Receiver<R>
5885    where
5886        F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static,
5887        Fut: Future<Output = R> + 'static,
5888        R: Send + 'static,
5889    {
5890        self.send_keyed_job(description, None, status, job)
5891    }
5892
5893    fn send_keyed_job<F, Fut, R>(
5894        &mut self,
5895        description: &'static str,
5896        key: Option<GitJobKey>,
5897        status: Option<SharedString>,
5898        job: F,
5899    ) -> oneshot::Receiver<R>
5900    where
5901        F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static,
5902        Fut: Future<Output = R> + 'static,
5903        R: Send + 'static,
5904    {
5905        let (result_tx, result_rx) = futures::channel::oneshot::channel();
5906        let job_id = post_inc(&mut self.job_id);
5907        let this = self.this.clone();
5908
5909        let key_label = key.as_ref().map(format_job_key);
5910        self.job_debug_queue.add(job_id, description, key_label);
5911
5912        self.job_sender
5913            .unbounded_send(GitJob {
5914                id: job_id,
5915                key,
5916                job: Box::new(move |state, cx: &mut AsyncApp| {
5917                    let job = job(state, cx.clone());
5918                    cx.spawn(async move |cx| {
5919                        this.update(cx, |this, cx| {
5920                            this.job_debug_queue.mark_running(job_id);
5921                            if let Some(s) = status {
5922                                this.active_jobs.insert(
5923                                    job_id,
5924                                    JobInfo {
5925                                        start: Instant::now(),
5926                                        message: s,
5927                                    },
5928                                );
5929                            }
5930                            cx.notify();
5931                        })
5932                        .ok();
5933
5934                        let result = job.await;
5935
5936                        this.update(cx, |this, cx| {
5937                            this.job_debug_queue.mark_complete(
5938                                job_id,
5939                                job_debug_queue::CompletedJobStatus::Finished,
5940                            );
5941                            this.active_jobs.remove(&job_id);
5942                            cx.notify();
5943                        })
5944                        .ok();
5945
5946                        result_tx.send(result).ok();
5947                    })
5948                }),
5949            })
5950            .ok();
5951        result_rx
5952    }
5953
5954    pub fn set_as_active_repository(&self, cx: &mut Context<Self>) {
5955        let Some(git_store) = self.git_store.upgrade() else {
5956            return;
5957        };
5958        let entity = cx.entity();
5959        git_store.update(cx, |git_store, cx| {
5960            let Some((&id, _)) = git_store
5961                .repositories
5962                .iter()
5963                .find(|(_, handle)| *handle == &entity)
5964            else {
5965                return;
5966            };
5967            git_store.active_repo_id = Some(id);
5968            cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
5969        });
5970    }
5971
5972    pub fn cached_status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
5973        self.snapshot.status()
5974    }
5975
5976    pub fn diff_stat_for_path(&self, path: &RepoPath) -> Option<DiffStat> {
5977        self.snapshot.diff_stat_for_path(path)
5978    }
5979
5980    pub fn cached_stash(&self) -> GitStash {
5981        self.snapshot.stash_entries.clone()
5982    }
5983
5984    pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option<ProjectPath> {
5985        let git_store = self.git_store.upgrade()?;
5986        let worktree_store = git_store.read(cx).worktree_store.read(cx);
5987        let abs_path = self.snapshot.repo_path_to_abs_path(path);
5988        let abs_path = SanitizedPath::new(&abs_path);
5989        let (worktree, relative_path) = worktree_store.find_worktree(abs_path, cx)?;
5990        Some(ProjectPath {
5991            worktree_id: worktree.read(cx).id(),
5992            path: relative_path,
5993        })
5994    }
5995
5996    pub fn project_path_to_repo_path(&self, path: &ProjectPath, cx: &App) -> Option<RepoPath> {
5997        let git_store = self.git_store.upgrade()?;
5998        let worktree_store = git_store.read(cx).worktree_store.read(cx);
5999        let abs_path = worktree_store.absolutize(path, cx)?;
6000        self.snapshot.abs_path_to_repo_path(&abs_path)
6001    }
6002
6003    pub fn contains_sub_repo(&self, other: &Entity<Self>, cx: &App) -> bool {
6004        other
6005            .read(cx)
6006            .snapshot
6007            .work_directory_abs_path
6008            .starts_with(&self.snapshot.work_directory_abs_path)
6009    }
6010
6011    pub fn commit_message_buffer(&self) -> Option<&Entity<Buffer>> {
6012        self.commit_message_buffer.as_ref()
6013    }
6014
6015    pub fn open_commit_buffer(
6016        &mut self,
6017        languages: Option<Arc<LanguageRegistry>>,
6018        buffer_store: Entity<BufferStore>,
6019        cx: &mut Context<Self>,
6020    ) -> Task<Result<Entity<Buffer>>> {
6021        let id = self.id;
6022        if let Some(buffer) = self.commit_message_buffer.clone() {
6023            return Task::ready(Ok(buffer));
6024        }
6025        let this = cx.weak_entity();
6026
6027        let rx = self.send_job(
6028            "open_commit_buffer",
6029            None,
6030            move |state, mut cx| async move {
6031                let Some(this) = this.upgrade() else {
6032                    bail!("git store was dropped");
6033                };
6034                match state {
6035                    RepositoryState::Local(..) => {
6036                        this.update(&mut cx, |_, cx| {
6037                            Self::open_local_commit_buffer(languages, buffer_store, cx)
6038                        })
6039                        .await
6040                    }
6041                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6042                        let request = client.request(proto::OpenCommitMessageBuffer {
6043                            project_id: project_id.0,
6044                            repository_id: id.to_proto(),
6045                        });
6046                        let response = request.await.context("requesting to open commit buffer")?;
6047                        let buffer_id = BufferId::new(response.buffer_id)?;
6048                        let buffer = buffer_store
6049                            .update(&mut cx, |buffer_store, cx| {
6050                                buffer_store.wait_for_remote_buffer(buffer_id, cx)
6051                            })
6052                            .await?;
6053                        if let Some(language_registry) = languages {
6054                            let git_commit_language =
6055                                language_registry.language_for_name("Git Commit").await?;
6056                            buffer.update(&mut cx, |buffer, cx| {
6057                                buffer.set_language(Some(git_commit_language), cx);
6058                            });
6059                        }
6060                        this.update(&mut cx, |this, _| {
6061                            this.commit_message_buffer = Some(buffer.clone());
6062                        });
6063                        Ok(buffer)
6064                    }
6065                }
6066            },
6067        );
6068
6069        cx.spawn(|_, _: &mut AsyncApp| async move { rx.await? })
6070    }
6071
6072    fn open_local_commit_buffer(
6073        language_registry: Option<Arc<LanguageRegistry>>,
6074        buffer_store: Entity<BufferStore>,
6075        cx: &mut Context<Self>,
6076    ) -> Task<Result<Entity<Buffer>>> {
6077        cx.spawn(async move |repository, cx| {
6078            let git_commit_language = match language_registry {
6079                Some(language_registry) => {
6080                    Some(language_registry.language_for_name("Git Commit").await?)
6081                }
6082                None => None,
6083            };
6084            let buffer = buffer_store
6085                .update(cx, |buffer_store, cx| {
6086                    buffer_store.create_buffer(git_commit_language, false, cx)
6087                })
6088                .await?;
6089
6090            repository.update(cx, |repository, _| {
6091                repository.commit_message_buffer = Some(buffer.clone());
6092            })?;
6093            Ok(buffer)
6094        })
6095    }
6096
6097    pub fn checkout_files(
6098        &mut self,
6099        commit: &str,
6100        paths: Vec<RepoPath>,
6101        cx: &mut Context<Self>,
6102    ) -> Task<Result<()>> {
6103        let commit = commit.to_string();
6104        let id = self.id;
6105
6106        self.spawn_job_with_tracking(
6107            paths.clone(),
6108            pending_op::GitStatus::Reverted,
6109            cx,
6110            async move |this, cx| {
6111                this.update(cx, |this, _cx| {
6112                    this.send_job(
6113                        "checkout_files",
6114                        Some(format!("git checkout {}", commit).into()),
6115                        move |git_repo, _| async move {
6116                            match git_repo {
6117                                RepositoryState::Local(LocalRepositoryState {
6118                                    backend,
6119                                    environment,
6120                                    ..
6121                                }) => {
6122                                    backend
6123                                        .checkout_files(commit, paths, environment.clone())
6124                                        .await
6125                                }
6126                                RepositoryState::Remote(RemoteRepositoryState {
6127                                    project_id,
6128                                    client,
6129                                }) => {
6130                                    client
6131                                        .request(proto::GitCheckoutFiles {
6132                                            project_id: project_id.0,
6133                                            repository_id: id.to_proto(),
6134                                            commit,
6135                                            paths: paths
6136                                                .into_iter()
6137                                                .map(|p| p.as_unix_str().to_owned())
6138                                                .collect(),
6139                                        })
6140                                        .await?;
6141
6142                                    Ok(())
6143                                }
6144                            }
6145                        },
6146                    )
6147                })?
6148                .await?
6149            },
6150        )
6151    }
6152
6153    pub fn reset(
6154        &mut self,
6155        commit: String,
6156        reset_mode: ResetMode,
6157        cx: &mut Context<Self>,
6158    ) -> oneshot::Receiver<Result<()>> {
6159        let id = self.id;
6160
6161        let receiver = self.send_job("reset", None, move |git_repo, _| async move {
6162            match git_repo {
6163                RepositoryState::Local(LocalRepositoryState {
6164                    backend,
6165                    environment,
6166                    ..
6167                }) => backend.reset(commit, reset_mode, environment).await,
6168                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6169                    client
6170                        .request(proto::GitReset {
6171                            project_id: project_id.0,
6172                            repository_id: id.to_proto(),
6173                            commit,
6174                            mode: match reset_mode {
6175                                ResetMode::Soft => git_reset::ResetMode::Soft.into(),
6176                                ResetMode::Mixed => git_reset::ResetMode::Mixed.into(),
6177                            },
6178                        })
6179                        .await?;
6180
6181                    Ok(())
6182                }
6183            }
6184        });
6185
6186        let scan_updates_tx =
6187            self.git_store()
6188                .and_then(|git_store| match &git_store.read(cx).state {
6189                    GitStoreState::Local { downstream, .. } => Some(
6190                        downstream
6191                            .as_ref()
6192                            .map(|downstream| downstream.updates_tx.clone()),
6193                    ),
6194                    _ => None,
6195                });
6196        if let Some(updates_tx) = scan_updates_tx {
6197            self.schedule_scan(updates_tx, cx);
6198        }
6199
6200        receiver
6201    }
6202
6203    pub fn show(&mut self, commit: String) -> oneshot::Receiver<Result<CommitDetails>> {
6204        let id = self.id;
6205        self.send_job("show", None, move |git_repo, _cx| async move {
6206            match git_repo {
6207                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6208                    backend.show(commit).await
6209                }
6210                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6211                    let resp = client
6212                        .request(proto::GitShow {
6213                            project_id: project_id.0,
6214                            repository_id: id.to_proto(),
6215                            commit,
6216                        })
6217                        .await?;
6218
6219                    Ok(CommitDetails {
6220                        sha: resp.sha.into(),
6221                        message: resp.message.into(),
6222                        commit_timestamp: resp.commit_timestamp,
6223                        author_email: resp.author_email.into(),
6224                        author_name: resp.author_name.into(),
6225                    })
6226                }
6227            }
6228        })
6229    }
6230
6231    pub fn load_commit_diff(&mut self, commit: String) -> oneshot::Receiver<Result<CommitDiff>> {
6232        let id = self.id;
6233        self.send_job("load_commit_diff", None, move |git_repo, cx| async move {
6234            match git_repo {
6235                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6236                    backend.load_commit(commit, cx).await
6237                }
6238                RepositoryState::Remote(RemoteRepositoryState {
6239                    client, project_id, ..
6240                }) => {
6241                    let response = client
6242                        .request(proto::LoadCommitDiff {
6243                            project_id: project_id.0,
6244                            repository_id: id.to_proto(),
6245                            commit,
6246                        })
6247                        .await?;
6248                    Ok(CommitDiff {
6249                        files: response
6250                            .files
6251                            .into_iter()
6252                            .map(|file| {
6253                                Ok(CommitFile {
6254                                    path: RepoPath::from_proto(&file.path)?,
6255                                    old_text: file.old_text,
6256                                    new_text: file.new_text,
6257                                    is_binary: file.is_binary,
6258                                })
6259                            })
6260                            .collect::<Result<Vec<_>>>()?,
6261                    })
6262                }
6263            }
6264        })
6265    }
6266
6267    pub fn file_history_changed_files(
6268        &mut self,
6269        paths: Vec<RepoPath>,
6270        commit_limit: usize,
6271    ) -> oneshot::Receiver<Result<Vec<FileHistoryChangedFileSets>>> {
6272        self.send_job(
6273            "file_history_changed_files",
6274            None,
6275            move |git_repo, _cx| async move {
6276                match git_repo {
6277                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6278                        backend
6279                            .file_history_changed_files(paths, commit_limit)
6280                            .await
6281                    }
6282                    RepositoryState::Remote(_) => {
6283                        anyhow::bail!("file history changed files is only supported locally")
6284                    }
6285                }
6286            },
6287        )
6288    }
6289
6290    pub fn get_graph_data(
6291        &self,
6292        log_source: LogSource,
6293        log_order: LogOrder,
6294    ) -> Option<&InitialGitGraphData> {
6295        self.initial_graph_data.get(&(log_source, log_order))
6296    }
6297
6298    pub fn search_commits(
6299        &mut self,
6300        log_source: LogSource,
6301        search_args: SearchCommitArgs,
6302        request_tx: async_channel::Sender<Oid>,
6303        cx: &mut Context<Self>,
6304    ) {
6305        let repository_state = self.repository_state.clone();
6306        let repository_id = self.id;
6307
6308        cx.background_spawn(async move {
6309            let repo_state = repository_state.await;
6310
6311            match repo_state {
6312                Ok(RepositoryState::Local(LocalRepositoryState { backend, .. })) => {
6313                    backend
6314                        .search_commits(log_source, search_args, request_tx)
6315                        .await
6316                        .log_err();
6317                }
6318
6319                Ok(RepositoryState::Remote(RemoteRepositoryState { client, project_id })) => {
6320                    let result = client
6321                        .request_stream(proto::SearchCommits {
6322                            project_id: project_id.to_proto(),
6323                            repository_id: repository_id.to_proto(),
6324                            log_source: Some(log_source_to_proto(&log_source)),
6325                            query: search_args.query.to_string(),
6326                            case_sensitive: search_args.case_sensitive,
6327                        })
6328                        .await;
6329
6330                    let mut stream = match result {
6331                        Ok(stream) => stream,
6332                        Err(error) => {
6333                            log::error!("failed to search commits remotely: {error:?}");
6334                            return;
6335                        }
6336                    };
6337
6338                    while let Some(response) = stream.next().await {
6339                        let response = match response {
6340                            Ok(response) => response,
6341                            Err(error) => {
6342                                log::error!(
6343                                    "failed to receive remote commit search results: {error:?}"
6344                                );
6345                                return;
6346                            }
6347                        };
6348
6349                        for sha in &response.shas {
6350                            let Ok(oid) = Oid::from_str(sha) else {
6351                                return;
6352                            };
6353                            if request_tx.send(oid).await.is_err() {
6354                                return;
6355                            }
6356                        }
6357                    }
6358                }
6359                Err(error) => {
6360                    log::error!("failed to get repository state for commit search: {error}");
6361                }
6362            };
6363        })
6364        .detach();
6365    }
6366
6367    pub fn graph_data(
6368        &mut self,
6369        log_source: LogSource,
6370        log_order: LogOrder,
6371        range: Range<usize>,
6372        cx: &mut Context<Self>,
6373    ) -> GraphDataResponse<'_> {
6374        let initial_commit_data = self
6375            .initial_graph_data
6376            .entry((log_source.clone(), log_order))
6377            .or_insert_with(|| {
6378                let state = self.repository_state.clone();
6379                let log_source = log_source.clone();
6380
6381                let fetch_task = cx.spawn(async move |repository, cx| {
6382                    let state = state.await;
6383                    let result = match state {
6384                        Ok(RepositoryState::Local(LocalRepositoryState { backend, .. })) => {
6385                            Self::local_git_graph_data(
6386                                repository.clone(),
6387                                backend,
6388                                log_source.clone(),
6389                                log_order,
6390                                cx,
6391                            )
6392                            .await
6393                        }
6394                        Ok(RepositoryState::Remote(remote)) => {
6395                            Self::remote_git_graph_data(
6396                                repository.clone(),
6397                                remote,
6398                                log_source.clone(),
6399                                log_order,
6400                                cx,
6401                            )
6402                            .await
6403                        }
6404                        Err(e) => Err(SharedString::from(e)),
6405                    };
6406
6407                    repository
6408                        .update(cx, |repository, cx| {
6409                            if let Some(data) = repository
6410                                .initial_graph_data
6411                                .get_mut(&(log_source.clone(), log_order))
6412                            {
6413                                match &result {
6414                                    Ok(()) => {
6415                                        cx.emit(RepositoryEvent::GraphEvent(
6416                                            (log_source.clone(), log_order),
6417                                            GitGraphEvent::FullyLoaded,
6418                                        ));
6419                                    }
6420                                    Err(fetch_task_error) => {
6421                                        data.subscribers.retain(|sender| {
6422                                            sender.try_send(Err(fetch_task_error.clone())).is_ok()
6423                                        });
6424                                        data.error = Some(fetch_task_error.clone());
6425                                        cx.emit(RepositoryEvent::GraphEvent(
6426                                            (log_source.clone(), log_order),
6427                                            GitGraphEvent::LoadingError,
6428                                        ));
6429                                    }
6430                                }
6431                                data.subscribers.clear();
6432                            } else {
6433                                debug_panic!(
6434                                    "This task would be dropped if this entry doesn't exist"
6435                                );
6436                            }
6437                        })
6438                        .log_err();
6439                });
6440
6441                InitialGitGraphData {
6442                    fetch_task,
6443                    error: None,
6444                    commit_data: Vec::new(),
6445                    commit_oid_to_index: HashMap::default(),
6446                    subscribers: Vec::new(),
6447                }
6448            });
6449
6450        let max_start = initial_commit_data.commit_data.len().saturating_sub(1);
6451        let max_end = initial_commit_data.commit_data.len();
6452
6453        GraphDataResponse {
6454            commits: &initial_commit_data.commit_data
6455                [range.start.min(max_start)..range.end.min(max_end)],
6456            is_loading: !initial_commit_data.fetch_task.is_ready(),
6457            error: initial_commit_data.error.clone(),
6458        }
6459    }
6460
6461    async fn append_initial_graph_commits(
6462        this: &WeakEntity<Self>,
6463        graph_data_key: &(LogSource, LogOrder),
6464        initial_graph_commit_data: Vec<Arc<InitialGraphCommitData>>,
6465        cx: &mut AsyncApp,
6466    ) {
6467        this.update(cx, |repository, cx| {
6468            let graph_data = repository
6469                .initial_graph_data
6470                .entry(graph_data_key.clone())
6471                .and_modify(|graph_data| {
6472                    if !graph_data.subscribers.is_empty() {
6473                        graph_data.subscribers.retain(|sender| {
6474                            sender
6475                                .try_send(Ok(initial_graph_commit_data.clone()))
6476                                .is_ok()
6477                        });
6478                    }
6479
6480                    for commit_data in initial_graph_commit_data {
6481                        graph_data
6482                            .commit_oid_to_index
6483                            .insert(commit_data.sha, graph_data.commit_data.len());
6484                        graph_data.commit_data.push(commit_data);
6485                    }
6486                    cx.emit(RepositoryEvent::GraphEvent(
6487                        graph_data_key.clone(),
6488                        GitGraphEvent::CountUpdated(graph_data.commit_data.len()),
6489                    ));
6490                });
6491
6492            match &graph_data {
6493                Entry::Occupied(_) => {}
6494                Entry::Vacant(_) => {
6495                    debug_panic!("This task should be dropped if data doesn't exist");
6496                }
6497            }
6498        })
6499        .log_err();
6500    }
6501
6502    async fn local_git_graph_data(
6503        this: WeakEntity<Self>,
6504        backend: Arc<dyn GitRepository>,
6505        log_source: LogSource,
6506        log_order: LogOrder,
6507        cx: &mut AsyncApp,
6508    ) -> Result<(), SharedString> {
6509        let (request_tx, request_rx) =
6510            async_channel::unbounded::<Vec<Arc<InitialGraphCommitData>>>();
6511
6512        let task = cx.background_executor().spawn({
6513            let log_source = log_source.clone();
6514            async move {
6515                backend
6516                    .initial_graph_data(log_source, log_order, request_tx)
6517                    .await
6518                    .map_err(|err| SharedString::from(err.to_string()))
6519            }
6520        });
6521
6522        let graph_data_key = (log_source, log_order);
6523
6524        while let Ok(initial_graph_commit_data) = request_rx.recv().await {
6525            Self::append_initial_graph_commits(
6526                &this,
6527                &graph_data_key,
6528                initial_graph_commit_data,
6529                cx,
6530            )
6531            .await;
6532        }
6533
6534        task.await?;
6535        Ok(())
6536    }
6537
6538    async fn remote_git_graph_data(
6539        this: WeakEntity<Self>,
6540        remote: RemoteRepositoryState,
6541        log_source: LogSource,
6542        log_order: LogOrder,
6543        cx: &mut AsyncApp,
6544    ) -> Result<(), SharedString> {
6545        let repository_id = this
6546            .update(cx, |repository, _| repository.id)
6547            .map_err(|err| SharedString::from(err.to_string()))?;
6548        let graph_data_key = (log_source.clone(), log_order);
6549        let mut response = remote
6550            .client
6551            .request_stream(proto::GetInitialGraphData {
6552                project_id: remote.project_id.to_proto(),
6553                repository_id: repository_id.to_proto(),
6554                log_source: Some(log_source_to_proto(&log_source)),
6555                log_order: log_order_to_proto(log_order),
6556            })
6557            .await
6558            .map_err(|err| SharedString::from(err.to_string()))?;
6559
6560        while let Some(response) = response.next().await {
6561            let response = response.map_err(|err| SharedString::from(err.to_string()))?;
6562            let commits = response
6563                .commits
6564                .into_iter()
6565                .map(initial_graph_commit_from_proto)
6566                .collect::<Result<Vec<_>>>()
6567                .map_err(|err| SharedString::from(err.to_string()))?;
6568            Self::append_initial_graph_commits(&this, &graph_data_key, commits, cx).await;
6569        }
6570
6571        Ok(())
6572    }
6573
6574    pub fn fetch_commit_data(
6575        &mut self,
6576        sha: Oid,
6577        await_result: bool,
6578        cx: &mut Context<Self>,
6579    ) -> &CommitDataState {
6580        if self.commit_data.contains_key(&sha) {
6581            let data = &self.commit_data[&sha];
6582
6583            if let CommitDataState::Loading(None) = data
6584                && await_result
6585            {
6586                let (tx, rx) = oneshot::channel();
6587                self.commit_data
6588                    .insert(sha, CommitDataState::Loading(Some(rx.shared())));
6589
6590                let handler = self.get_handler(cx);
6591                handler.completion_senders.insert(sha, tx);
6592            }
6593
6594            return &self.commit_data[&sha];
6595        }
6596
6597        let (state, completer) = if await_result {
6598            let (tx, rx) = oneshot::channel();
6599            (CommitDataState::Loading(Some(rx.shared())), Some(tx))
6600        } else {
6601            (CommitDataState::Loading(None), None)
6602        };
6603
6604        self.commit_data.insert(sha, state);
6605
6606        let handler = self.get_handler(cx);
6607        if let Some(tx) = completer {
6608            handler.completion_senders.insert(sha, tx);
6609        }
6610        let mut has_failed = false;
6611        if handler.commit_data_request.try_send(sha).is_ok() {
6612            handler.pending_requests.insert(sha);
6613        } else {
6614            has_failed = true;
6615            handler.completion_senders.remove(&sha);
6616            debug_assert!(
6617                matches!(
6618                    self.commit_data.remove(&sha),
6619                    Some(CommitDataState::Loading(_))
6620                ),
6621                "Commit data should still be loading when enqueueing the request fails"
6622            );
6623        }
6624
6625        &self.commit_data.get(&sha).unwrap_or_else(|| {
6626            debug_assert!(!has_failed, "This should always be inserted");
6627            &CommitDataState::Loading(None)
6628        })
6629    }
6630
6631    fn get_handler(&mut self, cx: &mut Context<Self>) -> &mut CommitDataHandler {
6632        if matches!(self.commit_data_handler, CommitDataHandlerState::Closed) {
6633            self.commit_data_handler =
6634                CommitDataHandlerState::Open(self.open_commit_data_handler(cx));
6635        }
6636
6637        match &mut self.commit_data_handler {
6638            CommitDataHandlerState::Open(handler) => handler,
6639            CommitDataHandlerState::Closed => unreachable!(),
6640        }
6641    }
6642
6643    fn open_commit_data_handler(&self, cx: &Context<Self>) -> CommitDataHandler {
6644        let state = self.repository_state.clone();
6645        let (result_tx, result_rx) = async_channel::bounded::<(Oid, CommitData)>(64);
6646        let (request_tx, request_rx) = async_channel::unbounded::<Oid>();
6647
6648        let foreground_task = cx.spawn(async move |this, cx| {
6649            while let Ok((sha, commit_data)) = result_rx.recv().await {
6650                let result = this.update(cx, |this, cx| {
6651                    let data = Arc::new(commit_data);
6652
6653                    if let CommitDataHandlerState::Open(handler) = &mut this.commit_data_handler {
6654                        handler.pending_requests.remove(&sha);
6655                        if let Some(completion_sender) = handler.completion_senders.remove(&sha) {
6656                            completion_sender.send(data.clone()).ok();
6657                        }
6658                    } else {
6659                        debug_panic!("The handler state has to be open for this task to exist");
6660                    }
6661
6662                    let old_value = this.commit_data.insert(sha, CommitDataState::Loaded(data));
6663                    debug_assert!(
6664                        !matches!(old_value, Some(CommitDataState::Loaded(_))),
6665                        "We should never overwrite commit data"
6666                    );
6667
6668                    cx.notify();
6669                });
6670                if result.is_err() {
6671                    break;
6672                }
6673            }
6674
6675            this.update(cx, |this, _cx| {
6676                let CommitDataHandlerState::Open(handler) = std::mem::replace(
6677                    &mut this.commit_data_handler,
6678                    CommitDataHandlerState::Closed,
6679                ) else {
6680                    debug_panic!("The handler state has to be open for this task to exist");
6681                    return;
6682                };
6683
6684                for sha in handler.pending_requests {
6685                    this.commit_data.remove(&sha);
6686                }
6687            })
6688            .ok();
6689        });
6690
6691        let request_tx_for_handler = request_tx;
6692        let repository_id = self.id;
6693        let background_executor = cx.background_executor().clone();
6694
6695        cx.background_spawn(async move {
6696            match state.await {
6697                Ok(RepositoryState::Local(LocalRepositoryState { backend, .. })) => {
6698                    Self::local_commit_data_reader(
6699                        backend,
6700                        request_rx,
6701                        result_tx,
6702                        background_executor,
6703                    )
6704                    .await;
6705                }
6706                Ok(RepositoryState::Remote(RemoteRepositoryState { project_id, client })) => {
6707                    Self::remote_commit_data_reader(
6708                        project_id,
6709                        client,
6710                        repository_id,
6711                        request_rx,
6712                        result_tx,
6713                        background_executor,
6714                    )
6715                    .await;
6716                }
6717                Err(error) => {
6718                    log::error!("failed to get repository state: {error}");
6719                    return;
6720                }
6721            };
6722        })
6723        .detach();
6724
6725        CommitDataHandler {
6726            _task: foreground_task,
6727            commit_data_request: request_tx_for_handler,
6728            completion_senders: HashMap::default(),
6729            pending_requests: HashSet::default(),
6730        }
6731    }
6732
6733    async fn local_commit_data_reader(
6734        backend: Arc<dyn GitRepository>,
6735        request_rx: smol::channel::Receiver<Oid>,
6736        result_tx: smol::channel::Sender<(Oid, CommitData)>,
6737        background_executor: BackgroundExecutor,
6738    ) {
6739        async fn receive_commit_data_request(
6740            request_rx: &smol::channel::Receiver<Oid>,
6741        ) -> Option<Oid> {
6742            if request_rx.is_closed() && request_rx.is_empty() {
6743                future::pending().await
6744            } else {
6745                request_rx.recv().await.ok()
6746            }
6747        }
6748
6749        let reader = match backend.commit_data_reader() {
6750            Ok(reader) => reader,
6751            Err(error) => {
6752                log::error!("failed to create commit data reader: {error:?}");
6753                return;
6754            }
6755        };
6756
6757        let read_commit_data = |sha| reader.read(sha).map(move |result| (sha, result));
6758        let mut read_futures = FuturesUnordered::new();
6759
6760        loop {
6761            if read_futures.is_empty() {
6762                let timeout = background_executor.timer(Duration::from_secs(10));
6763
6764                futures::select_biased! {
6765                    sha = futures::FutureExt::fuse(receive_commit_data_request(&request_rx)) => {
6766                        if let Some(sha) = sha {
6767                            read_futures.push(read_commit_data(sha));
6768                        }
6769                    }
6770                    _ = futures::FutureExt::fuse(timeout) => {
6771                        break;
6772                    }
6773                }
6774            }
6775
6776            let next_read = read_futures.next().fuse();
6777            futures::pin_mut!(next_read);
6778
6779            futures::select_biased! {
6780                result = next_read => {
6781                    let Some((sha, result)) = result else {
6782                        continue;
6783                    };
6784
6785                    match result {
6786                        Ok(commit_data) => {
6787                            if result_tx.send((sha, commit_data)).await.is_err() {
6788                                return;
6789                            }
6790                        }
6791                        Err(error) => {
6792                            log::error!("failed to read commit data for {sha}: {error:?}");
6793                        }
6794                    }
6795                }
6796                sha = futures::FutureExt::fuse(receive_commit_data_request(&request_rx)) => {
6797                    if let Some(sha) = sha {
6798                        read_futures.push(read_commit_data(sha));
6799                    }
6800                }
6801            }
6802        }
6803
6804        drop(result_tx);
6805    }
6806
6807    async fn remote_commit_data_reader(
6808        project_id: ProjectId,
6809        client: AnyProtoClient,
6810        repository_id: RepositoryId,
6811        request_rx: smol::channel::Receiver<Oid>,
6812        result_tx: smol::channel::Sender<(Oid, CommitData)>,
6813        background_executor: BackgroundExecutor,
6814    ) {
6815        let mut response_futures =
6816            FuturesUnordered::<BoxFuture<'static, Result<proto::GetCommitDataResponse>>>::new();
6817        let mut accept_requests = true;
6818        let mut next_request = Self::get_next_request(
6819            project_id,
6820            client.clone(),
6821            repository_id,
6822            &request_rx,
6823            &background_executor,
6824        )
6825        .boxed()
6826        .fuse();
6827
6828        loop {
6829            if !accept_requests && response_futures.is_empty() {
6830                break;
6831            }
6832
6833            if response_futures.is_empty() {
6834                match (&mut next_request).await {
6835                    NextCommitDataRequest::Request(request) => {
6836                        response_futures.push(request);
6837                        next_request = Self::get_next_request(
6838                            project_id,
6839                            client.clone(),
6840                            repository_id,
6841                            &request_rx,
6842                            &background_executor,
6843                        )
6844                        .boxed()
6845                        .fuse();
6846                    }
6847                    NextCommitDataRequest::Closed | NextCommitDataRequest::Idle => break,
6848                }
6849            }
6850
6851            let next_response = response_futures.next().fuse();
6852            futures::pin_mut!(next_response);
6853
6854            futures::select_biased! {
6855                request = next_request => {
6856                    match request {
6857                        NextCommitDataRequest::Request(request) => {
6858                            response_futures.push(request);
6859                        }
6860                        NextCommitDataRequest::Idle => {}
6861                        NextCommitDataRequest::Closed => {
6862                            accept_requests = false;
6863                        }
6864                    }
6865
6866                    if accept_requests {
6867                        next_request = Self::get_next_request(
6868                            project_id,
6869                            client.clone(),
6870                            repository_id,
6871                            &request_rx,
6872                            &background_executor,
6873                        )
6874                        .boxed()
6875                        .fuse();
6876                    }
6877                }
6878                result = next_response => {
6879                    let Some(result) = result else {
6880                        continue;
6881                    };
6882
6883                    if let Ok(commit_data) = result {
6884                        for commit in commit_data.commits {
6885                            let Ok(commit_data) = commit_data_from_proto(commit) else {
6886                                continue;
6887                            };
6888
6889                            if result_tx
6890                                .send((commit_data.sha, commit_data))
6891                                .await
6892                                .is_err()
6893                            {
6894                                return;
6895                            }
6896                        }
6897                    }
6898                }
6899            }
6900        }
6901
6902        drop(result_tx);
6903    }
6904
6905    async fn get_next_request(
6906        project_id: ProjectId,
6907        client: AnyProtoClient,
6908        repository_id: RepositoryId,
6909        request_rx: &smol::channel::Receiver<Oid>,
6910        background_executor: &BackgroundExecutor,
6911    ) -> NextCommitDataRequest {
6912        let mut queued_shas = Vec::with_capacity(64);
6913
6914        loop {
6915            if queued_shas.len() >= 64 {
6916                break;
6917            }
6918
6919            let timeout = background_executor.timer(Duration::from_millis(5));
6920
6921            futures::select_biased! {
6922                sha = futures::FutureExt::fuse(request_rx.recv()) => {
6923                    let Ok(sha) = sha else {
6924                        break;
6925                    };
6926
6927                    queued_shas.push(sha);
6928
6929                }
6930                _ = futures::FutureExt::fuse(timeout) => {
6931                    break;
6932                }
6933            }
6934        }
6935
6936        if queued_shas.is_empty() && request_rx.is_closed() {
6937            NextCommitDataRequest::Closed
6938        } else if queued_shas.is_empty() {
6939            NextCommitDataRequest::Idle
6940        } else {
6941            NextCommitDataRequest::Request(
6942                client
6943                    .request(proto::GetCommitData {
6944                        project_id: project_id.to_proto(),
6945                        repository_id: repository_id.to_proto(),
6946                        shas: queued_shas.into_iter().map(|oid| oid.to_string()).collect(),
6947                    })
6948                    .boxed(),
6949            )
6950        }
6951    }
6952
6953    fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
6954        Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
6955    }
6956
6957    fn save_buffers<'a>(
6958        &self,
6959        entries: impl IntoIterator<Item = &'a RepoPath>,
6960        cx: &mut Context<Self>,
6961    ) -> Vec<Task<anyhow::Result<()>>> {
6962        let mut save_futures = Vec::new();
6963        if let Some(buffer_store) = self.buffer_store(cx) {
6964            buffer_store.update(cx, |buffer_store, cx| {
6965                for path in entries {
6966                    let Some(project_path) = self.repo_path_to_project_path(path, cx) else {
6967                        continue;
6968                    };
6969                    if let Some(buffer) = buffer_store.get_by_path(&project_path)
6970                        && buffer
6971                            .read(cx)
6972                            .file()
6973                            .is_some_and(|file| file.disk_state().exists())
6974                        && buffer.read(cx).has_unsaved_edits()
6975                    {
6976                        save_futures.push(buffer_store.save_buffer(buffer, cx));
6977                    }
6978                }
6979            })
6980        }
6981        save_futures
6982    }
6983
6984    pub fn stage_entries(
6985        &mut self,
6986        entries: Vec<RepoPath>,
6987        cx: &mut Context<Self>,
6988    ) -> Task<anyhow::Result<()>> {
6989        self.stage_or_unstage_entries(true, entries, cx)
6990    }
6991
6992    pub fn unstage_entries(
6993        &mut self,
6994        entries: Vec<RepoPath>,
6995        cx: &mut Context<Self>,
6996    ) -> Task<anyhow::Result<()>> {
6997        self.stage_or_unstage_entries(false, entries, cx)
6998    }
6999
7000    fn stage_or_unstage_entries(
7001        &mut self,
7002        stage: bool,
7003        entries: Vec<RepoPath>,
7004        cx: &mut Context<Self>,
7005    ) -> Task<anyhow::Result<()>> {
7006        if entries.is_empty() {
7007            return Task::ready(Ok(()));
7008        }
7009        let Some(git_store) = self.git_store.upgrade() else {
7010            return Task::ready(Ok(()));
7011        };
7012        let id = self.id;
7013        let save_tasks = self.save_buffers(&entries, cx);
7014        let paths = entries
7015            .iter()
7016            .map(|p| p.as_unix_str())
7017            .collect::<Vec<_>>()
7018            .join(" ");
7019        let status = if stage {
7020            format!("git add {paths}")
7021        } else {
7022            format!("git reset {paths}")
7023        };
7024        let job_key = GitJobKey::WriteIndex(entries.clone());
7025
7026        self.spawn_job_with_tracking(
7027            entries.clone(),
7028            if stage {
7029                pending_op::GitStatus::Staged
7030            } else {
7031                pending_op::GitStatus::Unstaged
7032            },
7033            cx,
7034            async move |this, cx| {
7035                for save_task in save_tasks {
7036                    save_task.await?;
7037                }
7038
7039                this.update(cx, |this, cx| {
7040                    let weak_this = cx.weak_entity();
7041                    this.send_keyed_job(
7042                        "stage_or_unstage_entries",
7043                        Some(job_key),
7044                        Some(status.into()),
7045                        move |git_repo, mut cx| async move {
7046                            let hunk_staging_operation_counts = weak_this
7047                                .update(&mut cx, |this, cx| {
7048                                    let mut hunk_staging_operation_counts = HashMap::default();
7049                                    for path in &entries {
7050                                        let Some(project_path) =
7051                                            this.repo_path_to_project_path(path, cx)
7052                                        else {
7053                                            continue;
7054                                        };
7055                                        let Some(buffer) = git_store
7056                                            .read(cx)
7057                                            .buffer_store
7058                                            .read(cx)
7059                                            .get_by_path(&project_path)
7060                                        else {
7061                                            continue;
7062                                        };
7063                                        let Some(diff_state) = git_store
7064                                            .read(cx)
7065                                            .diffs
7066                                            .get(&buffer.read(cx).remote_id())
7067                                            .cloned()
7068                                        else {
7069                                            continue;
7070                                        };
7071                                        let buffer_snapshot = buffer.read(cx).text_snapshot();
7072                                        let hunk_staging_operation_count =
7073                                            diff_state.update(cx, |diff_state, cx| {
7074                                                diff_state
7075                                                    .mark_whole_file_stage_or_unstage_pending(
7076                                                        stage,
7077                                                        &buffer_snapshot,
7078                                                        cx,
7079                                                    );
7080                                                diff_state.hunk_staging_operation_count += 1;
7081                                                diff_state.hunk_staging_operation_count
7082                                            });
7083                                        hunk_staging_operation_counts.insert(
7084                                            diff_state.downgrade(),
7085                                            hunk_staging_operation_count,
7086                                        );
7087                                    }
7088                                    hunk_staging_operation_counts
7089                                })
7090                                .unwrap_or_default();
7091
7092                            let result = match git_repo {
7093                                RepositoryState::Local(LocalRepositoryState {
7094                                    backend,
7095                                    environment,
7096                                    ..
7097                                }) => {
7098                                    if stage {
7099                                        backend.stage_paths(entries, environment.clone()).await
7100                                    } else {
7101                                        backend.unstage_paths(entries, environment.clone()).await
7102                                    }
7103                                }
7104                                RepositoryState::Remote(RemoteRepositoryState {
7105                                    project_id,
7106                                    client,
7107                                }) => {
7108                                    if stage {
7109                                        client
7110                                            .request(proto::Stage {
7111                                                project_id: project_id.0,
7112                                                repository_id: id.to_proto(),
7113                                                paths: entries
7114                                                    .into_iter()
7115                                                    .map(|repo_path| {
7116                                                        repo_path.as_unix_str().to_owned()
7117                                                    })
7118                                                    .collect(),
7119                                            })
7120                                            .await
7121                                            .context("sending stage request")
7122                                            .map(|_| ())
7123                                    } else {
7124                                        client
7125                                            .request(proto::Unstage {
7126                                                project_id: project_id.0,
7127                                                repository_id: id.to_proto(),
7128                                                paths: entries
7129                                                    .into_iter()
7130                                                    .map(|repo_path| {
7131                                                        repo_path.as_unix_str().to_owned()
7132                                                    })
7133                                                    .collect(),
7134                                            })
7135                                            .await
7136                                            .context("sending unstage request")
7137                                            .map(|_| ())
7138                                    }
7139                                }
7140                            };
7141
7142                            for (diff_state, hunk_staging_operation_count) in
7143                                hunk_staging_operation_counts
7144                            {
7145                                diff_state
7146                                    .update(&mut cx, |diff_state, cx| {
7147                                        if result.is_ok() {
7148                                            diff_state.hunk_staging_operation_count_as_of_write =
7149                                                hunk_staging_operation_count;
7150                                        } else {
7151                                            diff_state.clear_pending_hunks(cx);
7152                                        }
7153                                    })
7154                                    .ok();
7155                            }
7156
7157                            result
7158                        },
7159                    )
7160                })?
7161                .await?
7162            },
7163        )
7164    }
7165
7166    pub fn stage_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
7167        let snapshot = self.snapshot.clone();
7168        let pending_ops = self.pending_ops.clone();
7169        let to_stage = cx.background_spawn(async move {
7170            snapshot
7171                .status()
7172                .filter_map(|entry| {
7173                    if let Some(ops) = pending_ops
7174                        .get(&PathKey(entry.repo_path.as_ref().clone()), ())
7175                        .filter(|ops| !ops.last_op_errored())
7176                    {
7177                        if ops.staging() || ops.staged() {
7178                            None
7179                        } else {
7180                            Some(entry.repo_path)
7181                        }
7182                    } else if entry.status.staging().is_fully_staged() {
7183                        None
7184                    } else {
7185                        Some(entry.repo_path)
7186                    }
7187                })
7188                .collect()
7189        });
7190
7191        cx.spawn(async move |this, cx| {
7192            let to_stage = to_stage.await;
7193            this.update(cx, |this, cx| {
7194                this.stage_or_unstage_entries(true, to_stage, cx)
7195            })?
7196            .await
7197        })
7198    }
7199
7200    pub fn unstage_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
7201        let snapshot = self.snapshot.clone();
7202        let pending_ops = self.pending_ops.clone();
7203        let to_unstage = cx.background_spawn(async move {
7204            snapshot
7205                .status()
7206                .filter_map(|entry| {
7207                    if let Some(ops) = pending_ops
7208                        .get(&PathKey(entry.repo_path.as_ref().clone()), ())
7209                        .filter(|ops| !ops.last_op_errored())
7210                    {
7211                        if !ops.staging() && !ops.staged() {
7212                            None
7213                        } else {
7214                            Some(entry.repo_path)
7215                        }
7216                    } else if entry.status.staging().is_fully_unstaged() {
7217                        None
7218                    } else {
7219                        Some(entry.repo_path)
7220                    }
7221                })
7222                .collect()
7223        });
7224
7225        cx.spawn(async move |this, cx| {
7226            let to_unstage = to_unstage.await;
7227            this.update(cx, |this, cx| {
7228                this.stage_or_unstage_entries(false, to_unstage, cx)
7229            })?
7230            .await
7231        })
7232    }
7233
7234    pub fn stash_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
7235        let to_stash = self.cached_status().map(|entry| entry.repo_path).collect();
7236
7237        self.stash_entries(to_stash, cx)
7238    }
7239
7240    pub fn stash_entries(
7241        &mut self,
7242        entries: Vec<RepoPath>,
7243        cx: &mut Context<Self>,
7244    ) -> Task<anyhow::Result<()>> {
7245        let id = self.id;
7246
7247        cx.spawn(async move |this, cx| {
7248            this.update(cx, |this, _| {
7249                this.send_job("stash_entries", None, move |git_repo, _cx| async move {
7250                    match git_repo {
7251                        RepositoryState::Local(LocalRepositoryState {
7252                            backend,
7253                            environment,
7254                            ..
7255                        }) => backend.stash_paths(entries, environment).await,
7256                        RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7257                            client
7258                                .request(proto::Stash {
7259                                    project_id: project_id.0,
7260                                    repository_id: id.to_proto(),
7261                                    paths: entries
7262                                        .into_iter()
7263                                        .map(|repo_path| repo_path.as_unix_str().to_owned())
7264                                        .collect(),
7265                                })
7266                                .await?;
7267                            Ok(())
7268                        }
7269                    }
7270                })
7271            })?
7272            .await??;
7273            Ok(())
7274        })
7275    }
7276
7277    pub fn stash_pop(
7278        &mut self,
7279        index: Option<usize>,
7280        cx: &mut Context<Self>,
7281    ) -> Task<anyhow::Result<()>> {
7282        let id = self.id;
7283        cx.spawn(async move |this, cx| {
7284            this.update(cx, |this, _| {
7285                this.send_job("stash_pop", None, move |git_repo, _cx| async move {
7286                    match git_repo {
7287                        RepositoryState::Local(LocalRepositoryState {
7288                            backend,
7289                            environment,
7290                            ..
7291                        }) => backend.stash_pop(index, environment).await,
7292                        RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7293                            client
7294                                .request(proto::StashPop {
7295                                    project_id: project_id.0,
7296                                    repository_id: id.to_proto(),
7297                                    stash_index: index.map(|i| i as u64),
7298                                })
7299                                .await
7300                                .context("sending stash pop request")?;
7301                            Ok(())
7302                        }
7303                    }
7304                })
7305            })?
7306            .await??;
7307            Ok(())
7308        })
7309    }
7310
7311    pub fn stash_apply(
7312        &mut self,
7313        index: Option<usize>,
7314        cx: &mut Context<Self>,
7315    ) -> Task<anyhow::Result<()>> {
7316        let id = self.id;
7317        cx.spawn(async move |this, cx| {
7318            this.update(cx, |this, _| {
7319                this.send_job("stash_apply", None, move |git_repo, _cx| async move {
7320                    match git_repo {
7321                        RepositoryState::Local(LocalRepositoryState {
7322                            backend,
7323                            environment,
7324                            ..
7325                        }) => backend.stash_apply(index, environment).await,
7326                        RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7327                            client
7328                                .request(proto::StashApply {
7329                                    project_id: project_id.0,
7330                                    repository_id: id.to_proto(),
7331                                    stash_index: index.map(|i| i as u64),
7332                                })
7333                                .await
7334                                .context("sending stash apply request")?;
7335                            Ok(())
7336                        }
7337                    }
7338                })
7339            })?
7340            .await??;
7341            Ok(())
7342        })
7343    }
7344
7345    pub fn add_path_to_gitignore(
7346        &mut self,
7347        repo_path: &RepoPath,
7348        is_dir: bool,
7349    ) -> oneshot::Receiver<Result<()>> {
7350        let id = self.id;
7351        let work_dir = self.snapshot.work_directory_abs_path.clone();
7352        let path_display = repo_path.as_ref().display(PathStyle::Unix);
7353        let path = repo_path.as_unix_str().to_owned();
7354        let file_path_str = if is_dir {
7355            format!("{}/", path_display)
7356        } else {
7357            path_display.to_string()
7358        };
7359
7360        self.send_job(
7361            "add_path_to_gitignore",
7362            None,
7363            move |git_repo, _cx| async move {
7364                match git_repo {
7365                    RepositoryState::Local(LocalRepositoryState { fs, .. }) => {
7366                        append_pattern_to_ignore_file(
7367                            fs,
7368                            work_dir.join(".gitignore"),
7369                            file_path_str,
7370                        )
7371                        .await
7372                    }
7373                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7374                        client
7375                            .request(proto::GitAddPathToGitignore {
7376                                project_id: project_id.0,
7377                                repository_id: id.to_proto(),
7378                                path,
7379                                is_dir,
7380                            })
7381                            .await
7382                            .context("sending add path to .gitignore request")?;
7383                        Ok(())
7384                    }
7385                }
7386            },
7387        )
7388    }
7389
7390    pub fn add_path_to_git_info_exclude(
7391        &mut self,
7392        repo_path: &RepoPath,
7393        is_dir: bool,
7394    ) -> oneshot::Receiver<Result<()>> {
7395        let id = self.id;
7396        let repository_dir = self.snapshot.repository_dir_abs_path.clone();
7397        let path_display = repo_path.as_ref().display(PathStyle::Unix);
7398        let path = repo_path.as_unix_str().to_owned();
7399        let file_path_str = if is_dir {
7400            format!("{}/", path_display)
7401        } else {
7402            path_display.to_string()
7403        };
7404
7405        self.send_job(
7406            "add_path_to_git_info_exclude",
7407            None,
7408            move |git_repo, _cx| async move {
7409                match git_repo {
7410                    RepositoryState::Local(LocalRepositoryState { fs, .. }) => {
7411                        append_pattern_to_ignore_file(
7412                            fs,
7413                            repository_dir.join(git::REPO_EXCLUDE),
7414                            file_path_str,
7415                        )
7416                        .await
7417                    }
7418                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7419                        client
7420                            .request(proto::GitAddPathToGitInfoExclude {
7421                                project_id: project_id.0,
7422                                repository_id: id.to_proto(),
7423                                path,
7424                                is_dir,
7425                            })
7426                            .await
7427                            .context("sending add path to .git/info/exclude request")?;
7428                        Ok(())
7429                    }
7430                }
7431            },
7432        )
7433    }
7434
7435    pub fn stash_drop(
7436        &mut self,
7437        index: Option<usize>,
7438        cx: &mut Context<Self>,
7439    ) -> oneshot::Receiver<anyhow::Result<()>> {
7440        let id = self.id;
7441        let updates_tx = self
7442            .git_store()
7443            .and_then(|git_store| match &git_store.read(cx).state {
7444                GitStoreState::Local { downstream, .. } => downstream
7445                    .as_ref()
7446                    .map(|downstream| downstream.updates_tx.clone()),
7447                _ => None,
7448            });
7449        let this = cx.weak_entity();
7450        self.send_job("stash_drop", None, move |git_repo, mut cx| async move {
7451            match git_repo {
7452                RepositoryState::Local(LocalRepositoryState {
7453                    backend,
7454                    environment,
7455                    ..
7456                }) => {
7457                    // TODO would be nice to not have to do this manually
7458                    let result = backend.stash_drop(index, environment).await;
7459                    if result.is_ok()
7460                        && let Ok(stash_entries) = backend.stash_entries().await
7461                    {
7462                        let snapshot = this.update(&mut cx, |this, cx| {
7463                            this.snapshot.stash_entries = stash_entries;
7464                            cx.emit(RepositoryEvent::StashEntriesChanged);
7465                            this.snapshot.clone()
7466                        })?;
7467                        if let Some(updates_tx) = updates_tx {
7468                            updates_tx
7469                                .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
7470                                .ok();
7471                        }
7472                    }
7473
7474                    result
7475                }
7476                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7477                    client
7478                        .request(proto::StashDrop {
7479                            project_id: project_id.0,
7480                            repository_id: id.to_proto(),
7481                            stash_index: index.map(|i| i as u64),
7482                        })
7483                        .await
7484                        .context("sending stash pop request")?;
7485                    Ok(())
7486                }
7487            }
7488        })
7489    }
7490
7491    // Kept for wire compatibility: older remote clients run the pre-commit hook explicitly
7492    // via `proto::RunGitHook` before committing. New code lets `git commit` run hooks itself.
7493    //
7494    // TODO: remove together with `proto::RunGitHook` once all supported peers commit without
7495    // sending it (see the deprecation note on the message in git.proto).
7496    pub fn run_hook(&mut self, hook: RunHook, _cx: &mut App) -> oneshot::Receiver<Result<()>> {
7497        let id = self.id;
7498        self.send_job(
7499            "run_hook",
7500            Some(format!("git hook {}", hook.as_str()).into()),
7501            move |git_repo, _cx| async move {
7502                match git_repo {
7503                    RepositoryState::Local(LocalRepositoryState {
7504                        backend,
7505                        environment,
7506                        ..
7507                    }) => backend.run_hook(hook, environment.clone()).await,
7508                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7509                        client
7510                            .request(proto::RunGitHook {
7511                                project_id: project_id.0,
7512                                repository_id: id.to_proto(),
7513                                hook: hook.to_proto(),
7514                            })
7515                            .await?;
7516
7517                        Ok(())
7518                    }
7519                }
7520            },
7521        )
7522    }
7523
7524    pub fn commit(
7525        &mut self,
7526        message: SharedString,
7527        name_and_email: Option<(SharedString, SharedString)>,
7528        options: CommitOptions,
7529        askpass: AskPassDelegate,
7530        _cx: &mut App,
7531    ) -> oneshot::Receiver<Result<()>> {
7532        let id = self.id;
7533        let askpass_delegates = self.askpass_delegates.clone();
7534        let askpass_id = util::post_inc(&mut self.latest_askpass_id);
7535
7536        self.send_job(
7537            "commit",
7538            Some("git commit".into()),
7539            move |git_repo, _cx| async move {
7540                match git_repo {
7541                    RepositoryState::Local(LocalRepositoryState {
7542                        backend,
7543                        environment,
7544                        ..
7545                    }) => {
7546                        backend
7547                            .commit(message, name_and_email, options, askpass, environment)
7548                            .await
7549                    }
7550                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7551                        askpass_delegates.lock().insert(askpass_id, askpass);
7552                        let _defer = util::defer(|| {
7553                            let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
7554                            debug_assert!(askpass_delegate.is_some());
7555                        });
7556                        let (name, email) = name_and_email.unzip();
7557                        client
7558                            .request(proto::Commit {
7559                                project_id: project_id.0,
7560                                repository_id: id.to_proto(),
7561                                message: String::from(message),
7562                                name: name.map(String::from),
7563                                email: email.map(String::from),
7564                                options: Some(proto::commit::CommitOptions {
7565                                    amend: options.amend,
7566                                    signoff: options.signoff,
7567                                    allow_empty: options.allow_empty,
7568                                    no_verify: options.no_verify,
7569                                }),
7570                                askpass_id,
7571                            })
7572                            .await?;
7573
7574                        Ok(())
7575                    }
7576                }
7577            },
7578        )
7579    }
7580
7581    async fn refresh_branch_list(
7582        this: &WeakEntity<Self>,
7583        backend: Arc<dyn GitRepository>,
7584        updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
7585        cx: &mut AsyncApp,
7586    ) -> Result<()> {
7587        let branches_scan = backend.branches().await?;
7588        let branch_list_error = branches_scan.error;
7589        let branch_list: Arc<[Branch]> = branches_scan.branches.into();
7590        let branch = branch_list.iter().find(|branch| branch.is_head).cloned();
7591        log::info!("head branch after scan is {branch:?}");
7592        let snapshot = this.update(cx, |this, cx| {
7593            let head_changed = branch != this.snapshot.branch;
7594            let branch_list_changed = *branch_list != *this.snapshot.branch_list;
7595            let branch_list_error_changed = this.snapshot.branch_list_error != branch_list_error;
7596            this.snapshot.branch = branch;
7597            this.snapshot.branch_list = branch_list;
7598            this.snapshot.branch_list_error = branch_list_error;
7599            if head_changed {
7600                cx.emit(RepositoryEvent::HeadChanged);
7601            }
7602            if branch_list_changed || branch_list_error_changed {
7603                cx.emit(RepositoryEvent::BranchListChanged);
7604            }
7605            this.snapshot.clone()
7606        })?;
7607        if let Some(updates_tx) = updates_tx {
7608            updates_tx
7609                .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
7610                .ok();
7611        }
7612        Ok(())
7613    }
7614
7615    pub fn fetch(
7616        &mut self,
7617        fetch_options: FetchOptions,
7618        askpass: AskPassDelegate,
7619        cx: &mut Context<Self>,
7620    ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
7621        let askpass_delegates = self.askpass_delegates.clone();
7622        let askpass_id = util::post_inc(&mut self.latest_askpass_id);
7623        let id = self.id;
7624
7625        let updates_tx = self
7626            .git_store()
7627            .and_then(|git_store| match &git_store.read(cx).state {
7628                GitStoreState::Local { downstream, .. } => downstream
7629                    .as_ref()
7630                    .map(|downstream| downstream.updates_tx.clone()),
7631                _ => None,
7632            });
7633
7634        let this = cx.weak_entity();
7635        self.send_job(
7636            "fetch",
7637            Some("git fetch".into()),
7638            move |git_repo, mut cx| async move {
7639                match git_repo {
7640                    RepositoryState::Local(LocalRepositoryState {
7641                        backend,
7642                        environment,
7643                        ..
7644                    }) => {
7645                        let result = backend
7646                            .fetch(fetch_options, askpass, environment, cx.clone())
7647                            .await;
7648                        if result.is_ok() {
7649                            Self::refresh_branch_list(&this, backend, updates_tx, &mut cx).await?;
7650                        }
7651                        result
7652                    }
7653                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7654                        askpass_delegates.lock().insert(askpass_id, askpass);
7655                        let _defer = util::defer(|| {
7656                            let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
7657                            debug_assert!(askpass_delegate.is_some());
7658                        });
7659
7660                        let response = client
7661                            .request(proto::Fetch {
7662                                project_id: project_id.0,
7663                                repository_id: id.to_proto(),
7664                                askpass_id,
7665                                remote: fetch_options.to_proto(),
7666                            })
7667                            .await?;
7668
7669                        Ok(RemoteCommandOutput {
7670                            stdout: response.stdout,
7671                            stderr: response.stderr,
7672                        })
7673                    }
7674                }
7675            },
7676        )
7677    }
7678
7679    pub fn push(
7680        &mut self,
7681        branch: SharedString,
7682        remote_branch: SharedString,
7683        remote: SharedString,
7684        options: Option<PushOptions>,
7685        askpass: AskPassDelegate,
7686        cx: &mut Context<Self>,
7687    ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
7688        let askpass_delegates = self.askpass_delegates.clone();
7689        let askpass_id = util::post_inc(&mut self.latest_askpass_id);
7690        let id = self.id;
7691
7692        let args = options
7693            .map(|option| match option {
7694                PushOptions::SetUpstream => " --set-upstream",
7695                PushOptions::Force => " --force-with-lease",
7696            })
7697            .unwrap_or("");
7698
7699        let updates_tx = self
7700            .git_store()
7701            .and_then(|git_store| match &git_store.read(cx).state {
7702                GitStoreState::Local { downstream, .. } => downstream
7703                    .as_ref()
7704                    .map(|downstream| downstream.updates_tx.clone()),
7705                _ => None,
7706            });
7707
7708        let this = cx.weak_entity();
7709        self.send_job(
7710            "push",
7711            Some(format!("git push {} {} {}:{}", args, remote, branch, remote_branch).into()),
7712            move |git_repo, mut cx| async move {
7713                match git_repo {
7714                    RepositoryState::Local(LocalRepositoryState {
7715                        backend,
7716                        environment,
7717                        ..
7718                    }) => {
7719                        let result = backend
7720                            .push(
7721                                branch.to_string(),
7722                                remote_branch.to_string(),
7723                                remote.to_string(),
7724                                options,
7725                                askpass,
7726                                environment.clone(),
7727                                cx.clone(),
7728                            )
7729                            .await;
7730                        // TODO would be nice to not have to do this manually
7731                        if result.is_ok() {
7732                            Self::refresh_branch_list(&this, backend, updates_tx, &mut cx).await?;
7733                        }
7734                        result
7735                    }
7736                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7737                        askpass_delegates.lock().insert(askpass_id, askpass);
7738                        let _defer = util::defer(|| {
7739                            let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
7740                            debug_assert!(askpass_delegate.is_some());
7741                        });
7742                        let response = client
7743                            .request(proto::Push {
7744                                project_id: project_id.0,
7745                                repository_id: id.to_proto(),
7746                                askpass_id,
7747                                branch_name: branch.to_string(),
7748                                remote_branch_name: remote_branch.to_string(),
7749                                remote_name: remote.to_string(),
7750                                options: options.map(|options| match options {
7751                                    PushOptions::Force => proto::push::PushOptions::Force,
7752                                    PushOptions::SetUpstream => {
7753                                        proto::push::PushOptions::SetUpstream
7754                                    }
7755                                }
7756                                    as i32),
7757                            })
7758                            .await?;
7759
7760                        Ok(RemoteCommandOutput {
7761                            stdout: response.stdout,
7762                            stderr: response.stderr,
7763                        })
7764                    }
7765                }
7766            },
7767        )
7768    }
7769
7770    pub fn pull(
7771        &mut self,
7772        branch: Option<SharedString>,
7773        remote: SharedString,
7774        rebase: bool,
7775        askpass: AskPassDelegate,
7776        _cx: &mut App,
7777    ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
7778        let askpass_delegates = self.askpass_delegates.clone();
7779        let askpass_id = util::post_inc(&mut self.latest_askpass_id);
7780        let id = self.id;
7781
7782        let mut status = "git pull".to_string();
7783        if rebase {
7784            status.push_str(" --rebase");
7785        }
7786        status.push_str(&format!(" {}", remote));
7787        if let Some(b) = &branch {
7788            status.push_str(&format!(" {}", b));
7789        }
7790
7791        self.send_job(
7792            "pull",
7793            Some(status.into()),
7794            move |git_repo, cx| async move {
7795                match git_repo {
7796                    RepositoryState::Local(LocalRepositoryState {
7797                        backend,
7798                        environment,
7799                        ..
7800                    }) => {
7801                        backend
7802                            .pull(
7803                                branch.as_ref().map(|b| b.to_string()),
7804                                remote.to_string(),
7805                                rebase,
7806                                askpass,
7807                                environment.clone(),
7808                                cx,
7809                            )
7810                            .await
7811                    }
7812                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7813                        askpass_delegates.lock().insert(askpass_id, askpass);
7814                        let _defer = util::defer(|| {
7815                            let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
7816                            debug_assert!(askpass_delegate.is_some());
7817                        });
7818                        let response = client
7819                            .request(proto::Pull {
7820                                project_id: project_id.0,
7821                                repository_id: id.to_proto(),
7822                                askpass_id,
7823                                rebase,
7824                                branch_name: branch.as_ref().map(|b| b.to_string()),
7825                                remote_name: remote.to_string(),
7826                            })
7827                            .await?;
7828
7829                        Ok(RemoteCommandOutput {
7830                            stdout: response.stdout,
7831                            stderr: response.stderr,
7832                        })
7833                    }
7834                }
7835            },
7836        )
7837    }
7838
7839    fn spawn_set_index_text_job(
7840        &mut self,
7841        path: RepoPath,
7842        content: Option<String>,
7843        hunk_staging_operation_count: Option<usize>,
7844        cx: &mut Context<Self>,
7845    ) -> oneshot::Receiver<anyhow::Result<()>> {
7846        let id = self.id;
7847        let this = cx.weak_entity();
7848        let git_store = self.git_store.clone();
7849        let abs_path = self.snapshot.repo_path_to_abs_path(&path);
7850        self.send_keyed_job(
7851            "spawn_set_index_text_job",
7852            Some(GitJobKey::WriteIndex(vec![path.clone()])),
7853            None,
7854            move |git_repo, mut cx| async move {
7855                log::debug!(
7856                    "start updating index text for buffer {}",
7857                    path.as_unix_str()
7858                );
7859
7860                match git_repo {
7861                    RepositoryState::Local(LocalRepositoryState {
7862                        fs,
7863                        backend,
7864                        environment,
7865                        ..
7866                    }) => {
7867                        let executable = match fs.metadata(&abs_path).await {
7868                            Ok(Some(meta)) => meta.is_executable,
7869                            Ok(None) => false,
7870                            Err(_err) => false,
7871                        };
7872                        backend
7873                            .set_index_text(path.clone(), content, environment.clone(), executable)
7874                            .await?;
7875                    }
7876                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7877                        client
7878                            .request(proto::SetIndexText {
7879                                project_id: project_id.0,
7880                                repository_id: id.to_proto(),
7881                                path: path.as_unix_str().to_owned(),
7882                                text: content,
7883                            })
7884                            .await?;
7885                    }
7886                }
7887                log::debug!(
7888                    "finish updating index text for buffer {}",
7889                    path.as_unix_str()
7890                );
7891
7892                if let Some(hunk_staging_operation_count) = hunk_staging_operation_count {
7893                    let project_path = this
7894                        .read_with(&cx, |this, cx| this.repo_path_to_project_path(&path, cx))
7895                        .ok()
7896                        .flatten();
7897                    git_store
7898                        .update(&mut cx, |git_store, cx| {
7899                            let buffer_id = git_store
7900                                .buffer_store
7901                                .read(cx)
7902                                .get_by_path(&project_path?)?
7903                                .read(cx)
7904                                .remote_id();
7905                            let diff_state = git_store.diffs.get(&buffer_id)?;
7906                            diff_state.update(cx, |diff_state, _| {
7907                                diff_state.hunk_staging_operation_count_as_of_write =
7908                                    hunk_staging_operation_count;
7909                            });
7910                            Some(())
7911                        })
7912                        .context("Git store dropped")?;
7913                }
7914                Ok(())
7915            },
7916        )
7917    }
7918
7919    pub fn create_remote(
7920        &mut self,
7921        remote_name: String,
7922        remote_url: String,
7923    ) -> oneshot::Receiver<Result<()>> {
7924        let id = self.id;
7925        self.send_job(
7926            "create_remote",
7927            Some(format!("git remote add {remote_name} {remote_url}").into()),
7928            move |repo, _cx| async move {
7929                match repo {
7930                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
7931                        backend.create_remote(remote_name, remote_url).await
7932                    }
7933                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7934                        client
7935                            .request(proto::GitCreateRemote {
7936                                project_id: project_id.0,
7937                                repository_id: id.to_proto(),
7938                                remote_name,
7939                                remote_url,
7940                            })
7941                            .await?;
7942
7943                        Ok(())
7944                    }
7945                }
7946            },
7947        )
7948    }
7949
7950    pub fn remove_remote(&mut self, remote_name: String) -> oneshot::Receiver<Result<()>> {
7951        let id = self.id;
7952        self.send_job(
7953            "remove_remote",
7954            Some(format!("git remove remote {remote_name}").into()),
7955            move |repo, _cx| async move {
7956                match repo {
7957                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
7958                        backend.remove_remote(remote_name).await
7959                    }
7960                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
7961                        client
7962                            .request(proto::GitRemoveRemote {
7963                                project_id: project_id.0,
7964                                repository_id: id.to_proto(),
7965                                remote_name,
7966                            })
7967                            .await?;
7968
7969                        Ok(())
7970                    }
7971                }
7972            },
7973        )
7974    }
7975
7976    pub fn get_remotes(
7977        &mut self,
7978        branch_name: Option<String>,
7979        is_push: bool,
7980    ) -> oneshot::Receiver<Result<Vec<Remote>>> {
7981        let id = self.id;
7982        self.send_job("get_remotes", None, move |repo, _cx| async move {
7983            match repo {
7984                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
7985                    let remote = if let Some(branch_name) = branch_name {
7986                        if is_push {
7987                            backend.get_push_remote(branch_name).await?
7988                        } else {
7989                            backend.get_branch_remote(branch_name).await?
7990                        }
7991                    } else {
7992                        None
7993                    };
7994
7995                    match remote {
7996                        Some(remote) => Ok(vec![remote]),
7997                        None => backend.get_all_remotes().await,
7998                    }
7999                }
8000                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8001                    let response = client
8002                        .request(proto::GetRemotes {
8003                            project_id: project_id.0,
8004                            repository_id: id.to_proto(),
8005                            branch_name,
8006                            is_push,
8007                        })
8008                        .await?;
8009
8010                    let remotes = response
8011                        .remotes
8012                        .into_iter()
8013                        .map(|remotes| Remote {
8014                            name: remotes.name.into(),
8015                        })
8016                        .collect();
8017
8018                    Ok(remotes)
8019                }
8020            }
8021        })
8022    }
8023
8024    pub fn remote_urls(&mut self) -> oneshot::Receiver<Result<HashMap<String, String>>> {
8025        let id = self.id;
8026        self.send_job("remote_urls", None, move |repo, _cx| async move {
8027            match repo {
8028                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8029                    Ok(backend.remote_urls().await)
8030                }
8031                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8032                    let response = client
8033                        .request(proto::GetRemotes {
8034                            project_id: project_id.0,
8035                            repository_id: id.to_proto(),
8036                            branch_name: None,
8037                            is_push: false,
8038                        })
8039                        .await?;
8040
8041                    Ok(response
8042                        .remotes
8043                        .into_iter()
8044                        .filter_map(|remote| Some((remote.name, remote.url?)))
8045                        .collect())
8046                }
8047            }
8048        })
8049    }
8050
8051    pub fn branches(&mut self) -> oneshot::Receiver<Result<BranchesScanResult>> {
8052        let id = self.id;
8053        self.send_job("branches", None, move |repo, _| async move {
8054            match repo {
8055                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8056                    backend.branches().await
8057                }
8058                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8059                    let response = client
8060                        .request(proto::GitGetBranches {
8061                            project_id: project_id.0,
8062                            repository_id: id.to_proto(),
8063                        })
8064                        .await?;
8065
8066                    let branches = response
8067                        .branches
8068                        .into_iter()
8069                        .map(|branch| proto_to_branch(&branch))
8070                        .collect();
8071
8072                    Ok(BranchesScanResult {
8073                        branches,
8074                        error: response.error.map(SharedString::from),
8075                    })
8076                }
8077            }
8078        })
8079    }
8080
8081    /// If this is a linked worktree (*NOT* the main checkout of a repository),
8082    /// returns the path for the linked worktree.
8083    ///
8084    /// Returns None if this is the main checkout.
8085    pub fn linked_worktree_path(&self) -> Option<&Arc<Path>> {
8086        self.snapshot
8087            .is_linked_worktree()
8088            .then_some(&self.work_directory_abs_path)
8089    }
8090
8091    pub fn path_for_new_linked_worktree(
8092        &self,
8093        branch_name: &str,
8094        worktree_directory_setting: &str,
8095    ) -> Result<PathBuf> {
8096        let repository_anchor = self
8097            .snapshot
8098            .main_worktree_abs_path()
8099            .unwrap_or(self.common_dir_abs_path.as_ref());
8100        let project_name = repository_anchor
8101            .file_name()
8102            .and_then(|name| name.to_str())
8103            .ok_or_else(|| anyhow!("git repo must have a directory name"))?;
8104        let directory = worktrees_directory_for_repo(
8105            repository_anchor,
8106            worktree_directory_setting,
8107            self.path_style,
8108        )?;
8109        let directory = self.path_style.join_path(&directory, branch_name)?;
8110        self.path_style.join_path(&directory, project_name)
8111    }
8112
8113    pub fn worktrees(&mut self) -> oneshot::Receiver<Result<Vec<GitWorktree>>> {
8114        let id = self.id;
8115        self.send_job("worktrees", None, move |repo, _| async move {
8116            match repo {
8117                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8118                    backend.worktrees().await
8119                }
8120                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8121                    let response = client
8122                        .request(proto::GitGetWorktrees {
8123                            project_id: project_id.0,
8124                            repository_id: id.to_proto(),
8125                        })
8126                        .await?;
8127
8128                    let worktrees = response
8129                        .worktrees
8130                        .into_iter()
8131                        .map(|worktree| proto_to_worktree(&worktree))
8132                        .collect();
8133
8134                    Ok(worktrees)
8135                }
8136            }
8137        })
8138    }
8139
8140    pub fn create_worktree(
8141        &mut self,
8142        target: CreateWorktreeTarget,
8143        path: PathBuf,
8144    ) -> oneshot::Receiver<Result<()>> {
8145        let id = self.id;
8146        let job_description = match target.branch_name() {
8147            Some(branch_name) => format!("git worktree add: {branch_name}"),
8148            None => "git worktree add (detached)".to_string(),
8149        };
8150        self.send_job(
8151            "create_worktree",
8152            Some(job_description.into()),
8153            move |repo, _cx| async move {
8154                match repo {
8155                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8156                        backend.create_worktree(target, path).await
8157                    }
8158                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8159                        let (name, commit, use_existing_branch) = match target {
8160                            CreateWorktreeTarget::ExistingBranch { branch_name } => {
8161                                (Some(branch_name), None, true)
8162                            }
8163                            CreateWorktreeTarget::NewBranch {
8164                                branch_name,
8165                                base_sha,
8166                            } => (Some(branch_name), base_sha, false),
8167                            CreateWorktreeTarget::Detached { base_sha } => (None, base_sha, false),
8168                        };
8169
8170                        client
8171                            .request(proto::GitCreateWorktree {
8172                                project_id: project_id.0,
8173                                repository_id: id.to_proto(),
8174                                name: name.unwrap_or_default(),
8175                                directory: path.to_string_lossy().to_string(),
8176                                commit,
8177                                use_existing_branch,
8178                            })
8179                            .await?;
8180
8181                        Ok(())
8182                    }
8183                }
8184            },
8185        )
8186    }
8187
8188    /// Returns the creation time of a linked worktree's git metadata
8189    /// directory. See [`GitRepository::worktree_created_at`]. For remote
8190    /// projects the stat runs on the remote host, where the worktree's
8191    /// filesystem lives.
8192    pub fn worktree_created_at(
8193        &mut self,
8194        worktree_path: PathBuf,
8195    ) -> oneshot::Receiver<Result<Option<SystemTime>>> {
8196        let id = self.id;
8197        self.send_job("worktree_created_at", None, move |repo, _cx| async move {
8198            match repo {
8199                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8200                    backend.worktree_created_at(worktree_path).await
8201                }
8202                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8203                    let response = client
8204                        .request(proto::GitWorktreeCreatedAt {
8205                            project_id: project_id.0,
8206                            repository_id: id.to_proto(),
8207                            worktree_path: worktree_path.to_string_lossy().to_string(),
8208                        })
8209                        .await?;
8210                    Ok(response.created_at.map(SystemTime::from))
8211                }
8212            }
8213        })
8214    }
8215
8216    pub fn create_worktree_detached(
8217        &mut self,
8218        path: PathBuf,
8219        commit: String,
8220    ) -> oneshot::Receiver<Result<()>> {
8221        self.create_worktree(
8222            CreateWorktreeTarget::Detached {
8223                base_sha: Some(commit),
8224            },
8225            path,
8226        )
8227    }
8228
8229    pub fn checkout_branch_in_worktree(
8230        &mut self,
8231        branch_name: String,
8232        worktree_path: PathBuf,
8233        create: bool,
8234    ) -> oneshot::Receiver<Result<()>> {
8235        let description = if create {
8236            format!("git checkout -b {branch_name}")
8237        } else {
8238            format!("git checkout {branch_name}")
8239        };
8240        self.send_job(
8241            "checkout_branch_in_worktree",
8242            Some(description.into()),
8243            move |repo, _cx| async move {
8244                match repo {
8245                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8246                        backend
8247                            .checkout_branch_in_worktree(branch_name, worktree_path, create)
8248                            .await
8249                    }
8250                    RepositoryState::Remote(_) => {
8251                        log::warn!(
8252                            "checkout_branch_in_worktree not supported for remote repositories"
8253                        );
8254                        Ok(())
8255                    }
8256                }
8257            },
8258        )
8259    }
8260
8261    pub fn head_sha(&mut self) -> oneshot::Receiver<Result<Option<String>>> {
8262        let id = self.id;
8263        self.send_job("head_sha", None, move |repo, _cx| async move {
8264            match repo {
8265                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8266                    Ok(backend.head_sha().await)
8267                }
8268                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8269                    let response = client
8270                        .request(proto::GitGetHeadSha {
8271                            project_id: project_id.0,
8272                            repository_id: id.to_proto(),
8273                        })
8274                        .await?;
8275
8276                    Ok(response.sha)
8277                }
8278            }
8279        })
8280    }
8281
8282    fn edit_ref(
8283        &mut self,
8284        ref_name: String,
8285        commit: Option<String>,
8286    ) -> oneshot::Receiver<Result<()>> {
8287        let id = self.id;
8288        self.send_job("edit_ref", None, move |repo, _cx| async move {
8289            match repo {
8290                RepositoryState::Local(LocalRepositoryState { backend, .. }) => match commit {
8291                    Some(commit) => backend.update_ref(ref_name, commit).await,
8292                    None => backend.delete_ref(ref_name).await,
8293                },
8294                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8295                    let action = match commit {
8296                        Some(sha) => proto::git_edit_ref::Action::UpdateToCommit(sha),
8297                        None => {
8298                            proto::git_edit_ref::Action::Delete(proto::git_edit_ref::DeleteRef {})
8299                        }
8300                    };
8301                    client
8302                        .request(proto::GitEditRef {
8303                            project_id: project_id.0,
8304                            repository_id: id.to_proto(),
8305                            ref_name,
8306                            action: Some(action),
8307                        })
8308                        .await?;
8309                    Ok(())
8310                }
8311            }
8312        })
8313    }
8314
8315    pub fn update_ref(
8316        &mut self,
8317        ref_name: String,
8318        commit: String,
8319    ) -> oneshot::Receiver<Result<()>> {
8320        self.edit_ref(ref_name, Some(commit))
8321    }
8322
8323    pub fn delete_ref(&mut self, ref_name: String) -> oneshot::Receiver<Result<()>> {
8324        self.edit_ref(ref_name, None)
8325    }
8326
8327    pub fn repair_worktrees(&mut self) -> oneshot::Receiver<Result<()>> {
8328        let id = self.id;
8329        self.send_job("repair_worktrees", None, move |repo, _cx| async move {
8330            match repo {
8331                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8332                    backend.repair_worktrees().await
8333                }
8334                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8335                    client
8336                        .request(proto::GitRepairWorktrees {
8337                            project_id: project_id.0,
8338                            repository_id: id.to_proto(),
8339                        })
8340                        .await?;
8341                    Ok(())
8342                }
8343            }
8344        })
8345    }
8346
8347    pub fn create_archive_checkpoint(&mut self) -> oneshot::Receiver<Result<(String, String)>> {
8348        let id = self.id;
8349        self.send_job(
8350            "create_archive_checkpoint",
8351            None,
8352            move |repo, _cx| async move {
8353                match repo {
8354                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8355                        backend.create_archive_checkpoint().await
8356                    }
8357                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8358                        let response = client
8359                            .request(proto::GitCreateArchiveCheckpoint {
8360                                project_id: project_id.0,
8361                                repository_id: id.to_proto(),
8362                            })
8363                            .await?;
8364                        Ok((response.staged_commit_sha, response.unstaged_commit_sha))
8365                    }
8366                }
8367            },
8368        )
8369    }
8370
8371    pub fn restore_archive_checkpoint(
8372        &mut self,
8373        staged_sha: String,
8374        unstaged_sha: String,
8375    ) -> oneshot::Receiver<Result<()>> {
8376        let id = self.id;
8377        self.send_job(
8378            "restore_archive_checkpoint",
8379            None,
8380            move |repo, _cx| async move {
8381                match repo {
8382                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8383                        backend
8384                            .restore_archive_checkpoint(staged_sha, unstaged_sha)
8385                            .await
8386                    }
8387                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8388                        client
8389                            .request(proto::GitRestoreArchiveCheckpoint {
8390                                project_id: project_id.0,
8391                                repository_id: id.to_proto(),
8392                                staged_commit_sha: staged_sha,
8393                                unstaged_commit_sha: unstaged_sha,
8394                            })
8395                            .await?;
8396                        Ok(())
8397                    }
8398                }
8399            },
8400        )
8401    }
8402
8403    pub fn remove_worktree(&mut self, path: PathBuf, force: bool) -> oneshot::Receiver<Result<()>> {
8404        let id = self.id;
8405        let repository_anchor_path: Arc<Path> = self
8406            .snapshot
8407            .main_worktree_abs_path()
8408            .unwrap_or(self.snapshot.common_dir_abs_path.as_ref())
8409            .into();
8410        self.send_job(
8411            "remove_worktree",
8412            Some(format!("git worktree remove: {}", path.display()).into()),
8413            move |repo, cx| async move {
8414                match repo {
8415                    RepositoryState::Local(LocalRepositoryState { backend, fs, .. }) => {
8416                        // When forcing, delete the worktree directory ourselves before
8417                        // invoking git. `git worktree remove` can remove the admin
8418                        // metadata in `.git/worktrees/<name>` but fail to delete the
8419                        // working directory (it continues past directory-removal errors),
8420                        // leaving an orphaned folder on disk. Deleting first guarantees
8421                        // the directory is gone, and `git worktree remove --force`
8422                        // tolerates a missing working tree while cleaning up the admin
8423                        // entry. We keep this inside the `Local` arm so that for remote
8424                        // projects the deletion runs on the remote machine (where the
8425                        // `GitRemoveWorktree` RPC is handled against the local repo on
8426                        // the headless server) using its own filesystem.
8427                        //
8428                        // After a successful removal, also delete any empty ancestor
8429                        // directories between the worktree path and the configured
8430                        // base directory used when creating linked worktrees.
8431                        //
8432                        // Non-force removals are left untouched before git runs:
8433                        // `git worktree remove` must see the dirty working tree to
8434                        // refuse the operation.
8435                        if force {
8436                            fs.remove_dir(
8437                                &path,
8438                                RemoveOptions {
8439                                    recursive: true,
8440                                    ignore_if_not_exists: true,
8441                                },
8442                            )
8443                            .await
8444                            .with_context(|| {
8445                                format!("failed to delete worktree directory '{}'", path.display())
8446                            })?;
8447                        }
8448
8449                        backend.remove_worktree(path.clone(), force).await?;
8450
8451                        let managed_worktree_base = cx.update(|cx| {
8452                            let setting = &ProjectSettings::get_global(cx).git.worktree_directory;
8453                            worktrees_directory_for_repo(
8454                                &repository_anchor_path,
8455                                setting,
8456                                PathStyle::local(),
8457                            )
8458                            .log_err()
8459                        });
8460
8461                        if let Some(managed_worktree_base) = managed_worktree_base {
8462                            remove_empty_managed_worktree_ancestors(
8463                                fs.as_ref(),
8464                                &path,
8465                                &managed_worktree_base,
8466                            )
8467                            .await;
8468                        }
8469
8470                        Ok(())
8471                    }
8472                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8473                        client
8474                            .request(proto::GitRemoveWorktree {
8475                                project_id: project_id.0,
8476                                repository_id: id.to_proto(),
8477                                path: path.to_string_lossy().to_string(),
8478                                force,
8479                            })
8480                            .await?;
8481
8482                        Ok(())
8483                    }
8484                }
8485            },
8486        )
8487    }
8488
8489    pub fn rename_worktree(
8490        &mut self,
8491        old_path: PathBuf,
8492        new_path: PathBuf,
8493    ) -> oneshot::Receiver<Result<()>> {
8494        let id = self.id;
8495        self.send_job(
8496            "rename_worktree",
8497            Some(format!("git worktree move: {}", old_path.display()).into()),
8498            move |repo, _cx| async move {
8499                match repo {
8500                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8501                        backend.rename_worktree(old_path, new_path).await
8502                    }
8503                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8504                        client
8505                            .request(proto::GitRenameWorktree {
8506                                project_id: project_id.0,
8507                                repository_id: id.to_proto(),
8508                                old_path: old_path.to_string_lossy().to_string(),
8509                                new_path: new_path.to_string_lossy().to_string(),
8510                            })
8511                            .await?;
8512
8513                        Ok(())
8514                    }
8515                }
8516            },
8517        )
8518    }
8519
8520    pub fn default_branch(
8521        &mut self,
8522        include_remote_name: bool,
8523    ) -> oneshot::Receiver<Result<Option<SharedString>>> {
8524        let id = self.id;
8525        self.send_job("default_branch", None, move |repo, _| async move {
8526            match repo {
8527                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8528                    backend.default_branch(include_remote_name).await
8529                }
8530                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8531                    let response = client
8532                        .request(proto::GetDefaultBranch {
8533                            project_id: project_id.0,
8534                            repository_id: id.to_proto(),
8535                            include_remote_name,
8536                        })
8537                        .await?;
8538
8539                    anyhow::Ok(response.branch.map(SharedString::from))
8540                }
8541            }
8542        })
8543    }
8544
8545    pub fn diff_tree(
8546        &mut self,
8547        diff_type: DiffTreeType,
8548        _cx: &App,
8549    ) -> oneshot::Receiver<Result<TreeDiff>> {
8550        let repository_id = self.snapshot.id;
8551        self.send_job("diff_tree", None, move |repo, _cx| async move {
8552            match repo {
8553                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8554                    backend.diff_tree(diff_type).await
8555                }
8556                RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => {
8557                    let response = client
8558                        .request(proto::GetTreeDiff {
8559                            project_id: project_id.0,
8560                            repository_id: repository_id.0,
8561                            is_merge: matches!(diff_type, DiffTreeType::MergeBase { .. }),
8562                            base: diff_type.base().to_string(),
8563                            head: diff_type.head().to_string(),
8564                        })
8565                        .await?;
8566
8567                    let entries = response
8568                        .entries
8569                        .into_iter()
8570                        .filter_map(|entry| {
8571                            let status = match entry.status() {
8572                                proto::tree_diff_status::Status::Added => TreeDiffStatus::Added,
8573                                proto::tree_diff_status::Status::Modified => {
8574                                    TreeDiffStatus::Modified {
8575                                        old: git::Oid::from_str(
8576                                            &entry.oid.context("missing oid").log_err()?,
8577                                        )
8578                                        .log_err()?,
8579                                    }
8580                                }
8581                                proto::tree_diff_status::Status::Deleted => {
8582                                    TreeDiffStatus::Deleted {
8583                                        old: git::Oid::from_str(
8584                                            &entry.oid.context("missing oid").log_err()?,
8585                                        )
8586                                        .log_err()?,
8587                                    }
8588                                }
8589                            };
8590                            Some((
8591                                RepoPath::from_rel_path(
8592                                    RelPath::from_unix_str(&entry.path).log_err()?,
8593                                ),
8594                                status,
8595                            ))
8596                        })
8597                        .collect();
8598
8599                    Ok(TreeDiff { entries })
8600                }
8601            }
8602        })
8603    }
8604
8605    pub fn diff(&mut self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver<Result<String>> {
8606        let id = self.id;
8607        self.send_job("diff", None, move |repo, _cx| async move {
8608            match repo {
8609                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8610                    backend.diff(diff_type).await
8611                }
8612                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8613                    let (proto_diff_type, merge_base_ref) = match &diff_type {
8614                        DiffType::HeadToIndex => {
8615                            (proto::git_diff::DiffType::HeadToIndex.into(), None)
8616                        }
8617                        DiffType::HeadToWorktree => {
8618                            (proto::git_diff::DiffType::HeadToWorktree.into(), None)
8619                        }
8620                        DiffType::MergeBase { base_ref } => (
8621                            proto::git_diff::DiffType::MergeBase.into(),
8622                            Some(base_ref.to_string()),
8623                        ),
8624                    };
8625                    let response = client
8626                        .request(proto::GitDiff {
8627                            project_id: project_id.0,
8628                            repository_id: id.to_proto(),
8629                            diff_type: proto_diff_type,
8630                            merge_base_ref,
8631                        })
8632                        .await?;
8633
8634                    Ok(response.diff)
8635                }
8636            }
8637        })
8638    }
8639
8640    pub fn create_branch(
8641        &mut self,
8642        branch_name: String,
8643        base_branch: Option<String>,
8644    ) -> oneshot::Receiver<Result<()>> {
8645        let id = self.id;
8646        let status_msg = if let Some(ref base) = base_branch {
8647            format!("git switch -c {branch_name} {base}").into()
8648        } else {
8649            format!("git switch -c {branch_name}").into()
8650        };
8651        self.send_job(
8652            "create_branch",
8653            Some(status_msg),
8654            move |repo, _cx| async move {
8655                match repo {
8656                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8657                        backend.create_branch(branch_name, base_branch).await
8658                    }
8659                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8660                        client
8661                            .request(proto::GitCreateBranch {
8662                                project_id: project_id.0,
8663                                repository_id: id.to_proto(),
8664                                branch_name,
8665                                base_branch,
8666                            })
8667                            .await?;
8668
8669                        Ok(())
8670                    }
8671                }
8672            },
8673        )
8674    }
8675
8676    pub fn change_branch(&mut self, branch_name: String) -> oneshot::Receiver<Result<()>> {
8677        let id = self.id;
8678        self.send_job(
8679            "change_branch",
8680            Some(format!("git switch {branch_name}").into()),
8681            move |repo, _cx| async move {
8682                match repo {
8683                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8684                        backend.change_branch(branch_name).await
8685                    }
8686                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8687                        client
8688                            .request(proto::GitChangeBranch {
8689                                project_id: project_id.0,
8690                                repository_id: id.to_proto(),
8691                                branch_name,
8692                            })
8693                            .await?;
8694
8695                        Ok(())
8696                    }
8697                }
8698            },
8699        )
8700    }
8701
8702    pub fn delete_branch(
8703        &mut self,
8704        is_remote: bool,
8705        branch_name: String,
8706        force: bool,
8707    ) -> oneshot::Receiver<Result<()>> {
8708        let id = self.id;
8709        let flag = delete_branch_flag(is_remote, force);
8710        self.send_job(
8711            "delete_branch",
8712            Some(format!("git branch {flag} {branch_name}").into()),
8713            move |repo, _cx| async move {
8714                match repo {
8715                    RepositoryState::Local(state) => {
8716                        state
8717                            .backend
8718                            .delete_branch(is_remote, branch_name, force)
8719                            .await
8720                    }
8721                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8722                        client
8723                            .request(proto::GitDeleteBranch {
8724                                project_id: project_id.0,
8725                                repository_id: id.to_proto(),
8726                                is_remote,
8727                                branch_name,
8728                                force,
8729                            })
8730                            .await?;
8731
8732                        Ok(())
8733                    }
8734                }
8735            },
8736        )
8737    }
8738
8739    pub fn rename_branch(
8740        &mut self,
8741        branch: String,
8742        new_name: String,
8743    ) -> oneshot::Receiver<Result<()>> {
8744        let id = self.id;
8745        self.send_job(
8746            "rename_branch",
8747            Some(format!("git branch -m {branch} {new_name}").into()),
8748            move |repo, _cx| async move {
8749                match repo {
8750                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8751                        backend.rename_branch(branch, new_name).await
8752                    }
8753                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8754                        client
8755                            .request(proto::GitRenameBranch {
8756                                project_id: project_id.0,
8757                                repository_id: id.to_proto(),
8758                                branch,
8759                                new_name,
8760                            })
8761                            .await?;
8762
8763                        Ok(())
8764                    }
8765                }
8766            },
8767        )
8768    }
8769
8770    pub fn check_for_pushed_commits(&mut self) -> oneshot::Receiver<Result<Vec<SharedString>>> {
8771        let id = self.id;
8772        self.send_job(
8773            "check_for_pushed_commits",
8774            None,
8775            move |repo, _cx| async move {
8776                match repo {
8777                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8778                        backend.check_for_pushed_commit().await
8779                    }
8780                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8781                        let response = client
8782                            .request(proto::CheckForPushedCommits {
8783                                project_id: project_id.0,
8784                                repository_id: id.to_proto(),
8785                            })
8786                            .await?;
8787
8788                        let branches = response.pushed_to.into_iter().map(Into::into).collect();
8789
8790                        Ok(branches)
8791                    }
8792                }
8793            },
8794        )
8795    }
8796
8797    pub fn checkpoint(&mut self) -> oneshot::Receiver<Result<GitRepositoryCheckpoint>> {
8798        let id = self.id;
8799        self.send_job("checkpoint", None, move |repo, _cx| async move {
8800            match repo {
8801                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8802                    backend.checkpoint().await
8803                }
8804                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8805                    let response = client
8806                        .request(proto::GitCreateCheckpoint {
8807                            project_id: project_id.0,
8808                            repository_id: id.to_proto(),
8809                        })
8810                        .await?;
8811
8812                    Ok(GitRepositoryCheckpoint {
8813                        commit_sha: Oid::from_bytes(&response.commit_sha)?,
8814                    })
8815                }
8816            }
8817        })
8818    }
8819
8820    pub fn restore_checkpoint(
8821        &mut self,
8822        checkpoint: GitRepositoryCheckpoint,
8823    ) -> oneshot::Receiver<Result<()>> {
8824        let id = self.id;
8825        self.send_job("restore_checkpoint", None, move |repo, _cx| async move {
8826            match repo {
8827                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8828                    backend.restore_checkpoint(checkpoint).await
8829                }
8830                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8831                    client
8832                        .request(proto::GitRestoreCheckpoint {
8833                            project_id: project_id.0,
8834                            repository_id: id.to_proto(),
8835                            commit_sha: checkpoint.commit_sha.as_bytes().to_vec(),
8836                        })
8837                        .await?;
8838                    Ok(())
8839                }
8840            }
8841        })
8842    }
8843
8844    pub(crate) fn apply_remote_update(
8845        &mut self,
8846        update: proto::UpdateRepository,
8847        cx: &mut Context<Self>,
8848    ) -> Result<()> {
8849        if let Some(repository_dir_abs_path) = &update.repository_dir_abs_path {
8850            self.snapshot.repository_dir_abs_path =
8851                Path::new(repository_dir_abs_path.as_str()).into();
8852        }
8853        if let Some(common_dir_abs_path) = &update.common_dir_abs_path {
8854            self.snapshot.common_dir_abs_path = Path::new(common_dir_abs_path.as_str()).into();
8855        }
8856
8857        let new_branch = update.branch_summary.as_ref().map(proto_to_branch);
8858        let new_head_commit = update
8859            .head_commit_details
8860            .as_ref()
8861            .map(proto_to_commit_details);
8862        if self.snapshot.branch != new_branch || self.snapshot.head_commit != new_head_commit {
8863            cx.emit(RepositoryEvent::HeadChanged)
8864        }
8865        self.snapshot.branch = new_branch;
8866        self.snapshot.head_commit = new_head_commit;
8867
8868        if update.is_last_update {
8869            let new_branch_list: Arc<[Branch]> =
8870                update.branch_list.iter().map(proto_to_branch).collect();
8871            let new_branch_list_error = update.branch_list_error.map(SharedString::from);
8872            if *self.snapshot.branch_list != *new_branch_list
8873                || self.snapshot.branch_list_error != new_branch_list_error
8874            {
8875                cx.emit(RepositoryEvent::BranchListChanged);
8876            }
8877            self.snapshot.branch_list = new_branch_list;
8878            self.snapshot.branch_list_error = new_branch_list_error;
8879        }
8880
8881        // We don't store any merge head state for downstream projects; the upstream
8882        // will track it and we will just get the updated conflicts
8883        let new_merge_heads = TreeMap::from_ordered_entries(
8884            update
8885                .current_merge_conflicts
8886                .into_iter()
8887                .filter_map(|path| Some((RepoPath::from_proto(&path).ok()?, vec![]))),
8888        );
8889        let conflicts_changed =
8890            self.snapshot.merge.merge_heads_by_conflicted_path != new_merge_heads;
8891        self.snapshot.merge.merge_heads_by_conflicted_path = new_merge_heads;
8892        self.snapshot.merge.message = update.merge_message.map(SharedString::from);
8893        let new_stash_entries = GitStash {
8894            entries: update
8895                .stash_entries
8896                .iter()
8897                .filter_map(|entry| proto_to_stash(entry).ok())
8898                .collect(),
8899        };
8900        if self.snapshot.stash_entries != new_stash_entries {
8901            cx.emit(RepositoryEvent::StashEntriesChanged)
8902        }
8903        self.snapshot.stash_entries = new_stash_entries;
8904        let new_linked_worktrees: Arc<[GitWorktree]> = update
8905            .linked_worktrees
8906            .iter()
8907            .map(proto_to_worktree)
8908            .collect();
8909        if *self.snapshot.linked_worktrees != *new_linked_worktrees {
8910            cx.emit(RepositoryEvent::GitWorktreeListChanged);
8911        }
8912        self.snapshot.linked_worktrees = new_linked_worktrees;
8913        self.snapshot.remote_upstream_url = update.remote_upstream_url;
8914        self.snapshot.remote_origin_url = update.remote_origin_url;
8915
8916        let edits = update
8917            .removed_statuses
8918            .into_iter()
8919            .filter_map(|path| {
8920                Some(sum_tree::Edit::Remove(PathKey(
8921                    RelPath::from_unix_str(&path).log_err()?.into(),
8922                )))
8923            })
8924            .chain(
8925                update
8926                    .updated_statuses
8927                    .into_iter()
8928                    .filter_map(|updated_status| {
8929                        Some(sum_tree::Edit::Insert(updated_status.try_into().log_err()?))
8930                    }),
8931            )
8932            .collect::<Vec<_>>();
8933        if conflicts_changed || !edits.is_empty() {
8934            cx.emit(RepositoryEvent::StatusesChanged);
8935        }
8936        self.snapshot.statuses_by_path.edit(edits, ());
8937
8938        if update.is_last_update {
8939            self.snapshot.scan_id = update.scan_id;
8940        }
8941        self.clear_pending_ops(cx);
8942        Ok(())
8943    }
8944
8945    pub fn compare_checkpoints(
8946        &mut self,
8947        left: GitRepositoryCheckpoint,
8948        right: GitRepositoryCheckpoint,
8949    ) -> oneshot::Receiver<Result<bool>> {
8950        let id = self.id;
8951        self.send_job("compare_checkpoints", None, move |repo, _cx| async move {
8952            match repo {
8953                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8954                    backend.compare_checkpoints(left, right).await
8955                }
8956                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8957                    let response = client
8958                        .request(proto::GitCompareCheckpoints {
8959                            project_id: project_id.0,
8960                            repository_id: id.to_proto(),
8961                            left_commit_sha: left.commit_sha.as_bytes().to_vec(),
8962                            right_commit_sha: right.commit_sha.as_bytes().to_vec(),
8963                        })
8964                        .await?;
8965                    Ok(response.equal)
8966                }
8967            }
8968        })
8969    }
8970
8971    pub fn diff_checkpoints(
8972        &mut self,
8973        base_checkpoint: GitRepositoryCheckpoint,
8974        target_checkpoint: GitRepositoryCheckpoint,
8975    ) -> oneshot::Receiver<Result<String>> {
8976        let id = self.id;
8977        self.send_job("diff_checkpoints", None, move |repo, _cx| async move {
8978            match repo {
8979                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
8980                    backend
8981                        .diff_checkpoints(base_checkpoint, target_checkpoint)
8982                        .await
8983                }
8984                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
8985                    let response = client
8986                        .request(proto::GitDiffCheckpoints {
8987                            project_id: project_id.0,
8988                            repository_id: id.to_proto(),
8989                            base_commit_sha: base_checkpoint.commit_sha.as_bytes().to_vec(),
8990                            target_commit_sha: target_checkpoint.commit_sha.as_bytes().to_vec(),
8991                        })
8992                        .await?;
8993                    Ok(response.diff)
8994                }
8995            }
8996        })
8997    }
8998
8999    fn clear_pending_ops(&mut self, cx: &mut Context<Self>) {
9000        let updated = SumTree::from_iter(
9001            self.pending_ops.iter().filter_map(|ops| {
9002                let inner_ops: Vec<PendingOp> =
9003                    ops.ops.iter().filter(|op| op.running()).cloned().collect();
9004                if inner_ops.is_empty() {
9005                    None
9006                } else {
9007                    Some(PendingOps {
9008                        repo_path: ops.repo_path.clone(),
9009                        ops: inner_ops,
9010                    })
9011                }
9012            }),
9013            (),
9014        );
9015
9016        if updated != self.pending_ops {
9017            cx.emit(RepositoryEvent::PendingOpsChanged {
9018                pending_ops: self.pending_ops.clone(),
9019            })
9020        }
9021
9022        self.pending_ops = updated;
9023    }
9024
9025    fn schedule_scan(
9026        &mut self,
9027        updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
9028        cx: &mut Context<Self>,
9029    ) {
9030        let this = cx.weak_entity();
9031        let _ = self.send_keyed_job(
9032            "schedule_scan",
9033            Some(GitJobKey::ReloadGitState),
9034            None,
9035            |state, mut cx| async move {
9036                log::debug!("run scheduled git status scan");
9037
9038                let Some(this) = this.upgrade() else {
9039                    return Ok(());
9040                };
9041                let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else {
9042                    bail!("not a local repository")
9043                };
9044                let snapshot = compute_snapshot(this.clone(), backend.clone(), &mut cx).await;
9045                this.update(&mut cx, |this, cx| {
9046                    this.clear_pending_ops(cx);
9047                });
9048                if let Some(updates_tx) = updates_tx {
9049                    updates_tx
9050                        .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
9051                        .ok();
9052                }
9053                Ok(())
9054            },
9055        );
9056    }
9057
9058    fn spawn_local_git_worker(
9059        state: Shared<Task<Result<LocalRepositoryState, String>>>,
9060        cx: &mut Context<Self>,
9061    ) -> (mpsc::UnboundedSender<GitJob>, Task<()>) {
9062        let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
9063
9064        let worker_task = cx.spawn(async move |this, cx| {
9065            let Some(state) = state.await.log_err() else {
9066                return;
9067            };
9068            if let Some(git_hosting_provider_registry) =
9069                cx.update(|cx| GitHostingProviderRegistry::try_global(cx))
9070            {
9071                git_hosting_providers::register_additional_providers(
9072                    git_hosting_provider_registry,
9073                    state.backend.clone(),
9074                )
9075                .await;
9076            }
9077            let state = RepositoryState::Local(state);
9078            let mut jobs = VecDeque::new();
9079            loop {
9080                while let Ok(next_job) = job_rx.try_recv() {
9081                    jobs.push_back(next_job);
9082                }
9083
9084                if let Some(job) = jobs.pop_front() {
9085                    if let Some(current_key) = &job.key
9086                        && jobs
9087                            .iter()
9088                            .any(|other_job| other_job.key.as_ref() == Some(current_key))
9089                    {
9090                        let skipped_job_id = job.id;
9091                        this.update(cx, |repo, _| {
9092                            repo.job_debug_queue.mark_complete(
9093                                skipped_job_id,
9094                                job_debug_queue::CompletedJobStatus::Skipped,
9095                            );
9096                        })
9097                        .ok();
9098                        continue;
9099                    }
9100                    (job.job)(state.clone(), cx).await;
9101                } else if let Some(job) = job_rx.next().await {
9102                    jobs.push_back(job);
9103                } else {
9104                    break;
9105                }
9106            }
9107        });
9108
9109        (job_tx, worker_task)
9110    }
9111
9112    fn spawn_remote_git_worker(
9113        state: RemoteRepositoryState,
9114        cx: &mut Context<Self>,
9115    ) -> (mpsc::UnboundedSender<GitJob>, Task<()>) {
9116        let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
9117
9118        let worker_task = cx.spawn(async move |this, cx| {
9119            let result: Result<()> = async {
9120                let state = RepositoryState::Remote(state);
9121                let mut jobs = VecDeque::new();
9122                loop {
9123                    while let Ok(next_job) = job_rx.try_recv() {
9124                        jobs.push_back(next_job);
9125                    }
9126
9127                    if let Some(job) = jobs.pop_front() {
9128                        if let Some(current_key) = &job.key
9129                            && jobs
9130                                .iter()
9131                                .any(|other_job| other_job.key.as_ref() == Some(current_key))
9132                        {
9133                            let skipped_job_id = job.id;
9134                            this.update(cx, |repo, _| {
9135                                repo.job_debug_queue.mark_complete(
9136                                    skipped_job_id,
9137                                    job_debug_queue::CompletedJobStatus::Skipped,
9138                                );
9139                            })
9140                            .ok();
9141                            continue;
9142                        }
9143                        (job.job)(state.clone(), cx).await;
9144                    } else if let Some(job) = job_rx.next().await {
9145                        jobs.push_back(job);
9146                    } else {
9147                        break;
9148                    }
9149                }
9150                anyhow::Ok(())
9151            }
9152            .await;
9153            result.log_err();
9154        });
9155
9156        (job_tx, worker_task)
9157    }
9158
9159    fn load_staged_text(
9160        &mut self,
9161        buffer_id: BufferId,
9162        repo_path: RepoPath,
9163        cx: &App,
9164    ) -> Task<Result<Option<String>>> {
9165        let rx = self.send_job("load_staged_text", None, move |state, _| async move {
9166            match state {
9167                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
9168                    anyhow::Ok(backend.load_index_text(repo_path).await)
9169                }
9170                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
9171                    let response = client
9172                        .request(proto::OpenUnstagedDiff {
9173                            project_id: project_id.to_proto(),
9174                            buffer_id: buffer_id.to_proto(),
9175                        })
9176                        .await?;
9177                    Ok(response.staged_text)
9178                }
9179            }
9180        });
9181        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
9182    }
9183
9184    fn load_committed_text(
9185        &mut self,
9186        buffer_id: BufferId,
9187        repo_path: RepoPath,
9188        cx: &App,
9189    ) -> Task<Result<DiffBasesChange>> {
9190        let rx = self.send_job("load_committed_text", None, move |state, _| async move {
9191            match state {
9192                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
9193                    let revisions = vec![
9194                        format!("HEAD:{}", repo_path.as_unix_str()),
9195                        format!(":{}", repo_path.as_unix_str()),
9196                    ];
9197                    let mut loaded_revisions = backend.load_revisions(revisions).await?.into_iter();
9198                    let committed_text = loaded_revisions.next().flatten();
9199                    let staged_text = loaded_revisions.next().flatten();
9200                    let diff_bases_change = if committed_text == staged_text {
9201                        DiffBasesChange::SetBoth(committed_text)
9202                    } else {
9203                        DiffBasesChange::SetEach {
9204                            index: staged_text,
9205                            head: committed_text,
9206                        }
9207                    };
9208                    anyhow::Ok(diff_bases_change)
9209                }
9210                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
9211                    use proto::open_uncommitted_diff_response::Mode;
9212
9213                    let response = client
9214                        .request(proto::OpenUncommittedDiff {
9215                            project_id: project_id.to_proto(),
9216                            buffer_id: buffer_id.to_proto(),
9217                        })
9218                        .await?;
9219                    let mode = Mode::from_i32(response.mode).context("Invalid mode")?;
9220                    let bases = match mode {
9221                        Mode::IndexMatchesHead => DiffBasesChange::SetBoth(response.committed_text),
9222                        Mode::IndexAndHead => DiffBasesChange::SetEach {
9223                            head: response.committed_text,
9224                            index: response.staged_text,
9225                        },
9226                    };
9227                    Ok(bases)
9228                }
9229            }
9230        });
9231
9232        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
9233    }
9234
9235    pub fn load_commit_template_text(
9236        &mut self,
9237    ) -> oneshot::Receiver<Result<Option<GitCommitTemplate>>> {
9238        let repository_id = self.snapshot.id;
9239        self.send_job(
9240            "load_commit_template_text",
9241            None,
9242            move |git_repo, _cx| async move {
9243                match git_repo {
9244                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
9245                        backend.load_commit_template().await
9246                    }
9247                    RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => {
9248                        let response = client
9249                            .request(proto::LoadCommitTemplate {
9250                                project_id: project_id.to_proto(),
9251                                repository_id: repository_id.0,
9252                            })
9253                            .await?;
9254                        Ok(response
9255                            .template
9256                            .map(|template| GitCommitTemplate { template }))
9257                    }
9258                }
9259            },
9260        )
9261    }
9262
9263    fn load_blob_content(&mut self, oid: Oid, cx: &App) -> Task<Result<String>> {
9264        let repository_id = self.snapshot.id;
9265        let rx = self.send_job("load_blob_content", None, move |state, _| async move {
9266            match state {
9267                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
9268                    backend.load_blob_content(oid).await
9269                }
9270                RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => {
9271                    let response = client
9272                        .request(proto::GetBlobContent {
9273                            project_id: project_id.to_proto(),
9274                            repository_id: repository_id.0,
9275                            oid: oid.to_string(),
9276                        })
9277                        .await?;
9278                    Ok(response.content)
9279                }
9280            }
9281        });
9282        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
9283    }
9284
9285    fn paths_changed(
9286        &mut self,
9287        paths: Vec<RepoPath>,
9288        updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
9289        cx: &mut Context<Self>,
9290    ) {
9291        if !paths.is_empty() {
9292            self.paths_needing_status_update.push(paths);
9293        }
9294
9295        let this = cx.weak_entity();
9296        let _ = self.send_keyed_job(
9297            "paths_changed",
9298            Some(GitJobKey::RefreshStatuses),
9299            None,
9300            |state, mut cx| async move {
9301                let (prev_snapshot, changed_paths) = this.update(&mut cx, |this, _| {
9302                    (
9303                        this.snapshot.clone(),
9304                        mem::take(&mut this.paths_needing_status_update),
9305                    )
9306                })?;
9307                let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else {
9308                    bail!("not a local repository")
9309                };
9310
9311                if changed_paths.is_empty() {
9312                    return Ok(());
9313                }
9314
9315                let has_head = prev_snapshot.head_commit.is_some();
9316
9317                let changed_path_statuses = cx
9318                    .background_spawn(async move {
9319                        let changed_paths = GitStore::coalesce_repo_paths(
9320                            changed_paths
9321                                .into_iter()
9322                                .flatten()
9323                                .collect::<BTreeSet<_>>()
9324                                .into_iter()
9325                                .collect(),
9326                        );
9327                        let changed_paths_vec = changed_paths.iter().cloned().collect::<Vec<_>>();
9328
9329                        let status_task = backend.status(&changed_paths_vec);
9330                        let diff_stat_future = |diff| {
9331                            if has_head {
9332                                backend.diff_stat(diff, &changed_paths_vec)
9333                            } else {
9334                                future::ready(Ok(status::GitDiffStat::default())).boxed()
9335                            }
9336                        };
9337
9338                        let (statuses, diff_stats, staged_diff_stats, unstaged_diff_stats) =
9339                            futures::future::try_join4(
9340                                status_task,
9341                                diff_stat_future(DiffStatType::HeadToWorktree),
9342                                diff_stat_future(DiffStatType::HeadToIndex),
9343                                diff_stat_future(DiffStatType::IndexToWorktree),
9344                            )
9345                            .await?;
9346
9347                        let diff_stats: HashMap<RepoPath, DiffStat> =
9348                            HashMap::from_iter(diff_stats.entries.into_iter().cloned());
9349                        let staged_diff_stats: HashMap<RepoPath, DiffStat> =
9350                            HashMap::from_iter(staged_diff_stats.entries.into_iter().cloned());
9351                        let unstaged_diff_stats: HashMap<RepoPath, DiffStat> =
9352                            HashMap::from_iter(unstaged_diff_stats.entries.into_iter().cloned());
9353
9354                        let mut changed_path_statuses = Vec::new();
9355                        let prev_statuses = prev_snapshot.statuses_by_path.clone();
9356                        let current_status_paths = statuses
9357                            .entries
9358                            .iter()
9359                            .map(|(repo_path, _)| repo_path.clone())
9360                            .collect::<BTreeSet<_>>();
9361
9362                        for path in &changed_paths {
9363                            let mut cursor = prev_statuses.cursor::<PathProgress>(());
9364                            cursor.seek_forward(&PathTarget::Path(path), Bias::Left);
9365                            while let Some(entry) = cursor.item() {
9366                                if !entry.repo_path.starts_with(path) {
9367                                    break;
9368                                }
9369
9370                                if !current_status_paths.contains(&entry.repo_path) {
9371                                    changed_path_statuses.push(Edit::Remove(PathKey(
9372                                        entry.repo_path.as_ref().clone(),
9373                                    )));
9374                                }
9375                                cursor.next();
9376                            }
9377                        }
9378
9379                        let mut cursor = prev_statuses.cursor::<PathProgress>(());
9380
9381                        for (repo_path, status) in &*statuses.entries {
9382                            let current_diff_stat = diff_stats.get(repo_path).copied();
9383                            let current_staged_diff_stat =
9384                                staged_diff_stats.get(repo_path).copied();
9385                            let current_unstaged_diff_stat =
9386                                unstaged_diff_stats.get(repo_path).copied();
9387
9388                            if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left)
9389                                && cursor.item().is_some_and(|entry| {
9390                                    entry.status == *status
9391                                        && entry.diff_stat == current_diff_stat
9392                                        && entry.staged_diff_stat == current_staged_diff_stat
9393                                        && entry.unstaged_diff_stat == current_unstaged_diff_stat
9394                                })
9395                            {
9396                                continue;
9397                            }
9398
9399                            changed_path_statuses.push(Edit::Insert(StatusEntry {
9400                                repo_path: repo_path.clone(),
9401                                status: *status,
9402                                diff_stat: current_diff_stat,
9403                                staged_diff_stat: current_staged_diff_stat,
9404                                unstaged_diff_stat: current_unstaged_diff_stat,
9405                            }));
9406                        }
9407                        anyhow::Ok(changed_path_statuses)
9408                    })
9409                    .await?;
9410
9411                this.update(&mut cx, |this, cx| {
9412                    if !changed_path_statuses.is_empty() {
9413                        cx.emit(RepositoryEvent::StatusesChanged);
9414                        this.snapshot
9415                            .statuses_by_path
9416                            .edit(changed_path_statuses, ());
9417                        this.snapshot.scan_id += 1;
9418                    }
9419
9420                    if let Some(updates_tx) = updates_tx {
9421                        updates_tx
9422                            .unbounded_send(DownstreamUpdate::UpdateRepository(
9423                                this.snapshot.clone(),
9424                            ))
9425                            .ok();
9426                    }
9427                })
9428            },
9429        );
9430    }
9431
9432    /// currently running git command and when it started
9433    pub fn current_job(&self) -> Option<JobInfo> {
9434        self.active_jobs.values().next().cloned()
9435    }
9436
9437    pub fn job_debug_queue(&self) -> &job_debug_queue::GitJobDebugQueue {
9438        &self.job_debug_queue
9439    }
9440
9441    pub fn barrier(&mut self) -> oneshot::Receiver<()> {
9442        self.send_job("barrier", None, |_, _| async {})
9443    }
9444
9445    fn spawn_job_with_tracking<AsyncFn>(
9446        &mut self,
9447        paths: Vec<RepoPath>,
9448        git_status: pending_op::GitStatus,
9449        cx: &mut Context<Self>,
9450        f: AsyncFn,
9451    ) -> Task<Result<()>>
9452    where
9453        AsyncFn: AsyncFnOnce(WeakEntity<Repository>, &mut AsyncApp) -> Result<()> + 'static,
9454    {
9455        let ids = self.new_pending_ops_for_paths(paths, git_status);
9456
9457        cx.spawn(async move |this, cx| {
9458            let (job_status, result) = match f(this.clone(), cx).await {
9459                Ok(()) => (pending_op::JobStatus::Finished, Ok(())),
9460                Err(err) if err.is::<Canceled>() => (pending_op::JobStatus::Skipped, Ok(())),
9461                Err(err) => (pending_op::JobStatus::Error, Err(err)),
9462            };
9463
9464            this.update(cx, |this, _| {
9465                let mut edits = Vec::with_capacity(ids.len());
9466                for (id, entry) in ids {
9467                    if let Some(mut ops) = this
9468                        .pending_ops
9469                        .get(&PathKey(entry.as_ref().clone()), ())
9470                        .cloned()
9471                    {
9472                        if let Some(op) = ops.op_by_id_mut(id) {
9473                            op.job_status = job_status;
9474                        }
9475                        edits.push(sum_tree::Edit::Insert(ops));
9476                    }
9477                }
9478                this.pending_ops.edit(edits, ());
9479            })?;
9480
9481            result
9482        })
9483    }
9484
9485    fn new_pending_ops_for_paths(
9486        &mut self,
9487        paths: Vec<RepoPath>,
9488        git_status: pending_op::GitStatus,
9489    ) -> Vec<(PendingOpId, RepoPath)> {
9490        let mut edits = Vec::with_capacity(paths.len());
9491        let mut ids = Vec::with_capacity(paths.len());
9492        for path in paths {
9493            let mut ops = self
9494                .pending_ops
9495                .get(&PathKey(path.as_ref().clone()), ())
9496                .cloned()
9497                .unwrap_or_else(|| PendingOps::new(&path));
9498            let id = ops.max_id() + 1;
9499            ops.ops.push(PendingOp {
9500                id,
9501                git_status,
9502                job_status: pending_op::JobStatus::Running,
9503            });
9504            edits.push(sum_tree::Edit::Insert(ops));
9505            ids.push((id, path));
9506        }
9507        self.pending_ops.edit(edits, ());
9508        ids
9509    }
9510
9511    pub fn access(&mut self, _cx: &App) -> oneshot::Receiver<GitAccess> {
9512        self.send_job("access", None, move |git_repo, _cx| async move {
9513            match git_repo {
9514                // TODO: Correctly handle remote repositories, where the user
9515                // that's running the Zed remote may not own the `.git/`
9516                // directory. For now we just return `GitAccess::Yes` so that
9517                // remoting continues working as expected.
9518                RepositoryState::Remote(..) => GitAccess::Yes,
9519                RepositoryState::Local(state) => match state.backend.check_access().await {
9520                    Ok(_) => GitAccess::Yes,
9521                    Err(_) => GitAccess::No,
9522                },
9523            }
9524        })
9525    }
9526
9527    pub fn default_remote_url(&self) -> Option<String> {
9528        self.remote_upstream_url
9529            .clone()
9530            .or(self.remote_origin_url.clone())
9531    }
9532}
9533
9534fn format_job_key(key: &GitJobKey) -> SharedString {
9535    match key {
9536        GitJobKey::WriteIndex(paths) => {
9537            let paths_str: Vec<_> = paths
9538                .iter()
9539                .map(|p| {
9540                    let rel: &RelPath = p;
9541                    rel.display(PathStyle::local())
9542                })
9543                .collect();
9544            format!("WriteIndex({})", paths_str.join(", ")).into()
9545        }
9546        GitJobKey::ReloadBufferDiffBases => "ReloadBufferDiffBases".into(),
9547        GitJobKey::RefreshStatuses => "RefreshStatuses".into(),
9548        GitJobKey::ReloadGitState => "ReloadGitState".into(),
9549    }
9550}
9551
9552/// If `path` is a git linked worktree checkout, resolves it to the main
9553/// repository's identity path. For regular linked worktrees this is the main
9554/// repository's working directory; for linked worktrees backed by a bare repo
9555/// such as `.bare`, this is the parent project directory users think of as the
9556/// repository root. Returns `None` if `path` is a normal repository, not a git
9557/// repo, or if resolution fails.
9558///
9559/// Resolution works by:
9560/// 1. Reading the `.git` file to get the `gitdir:` pointer
9561/// 2. Following that to the worktree-specific git directory
9562/// 3. Reading the `commondir` file to find the shared `.git` directory
9563/// 4. Deriving the main repo's identity path from the common dir
9564pub async fn resolve_git_worktree_to_main_repo(fs: &dyn Fs, path: &Path) -> Option<PathBuf> {
9565    let dot_git = path.join(".git");
9566    let metadata = fs.metadata(&dot_git).await.ok()??;
9567    if metadata.is_dir {
9568        return None; // Normal repo, not a linked worktree
9569    }
9570    // It's a .git file — parse the gitdir: pointer
9571    let content = fs.load(&dot_git).await.ok()?;
9572    let gitdir_rel = content.strip_prefix("gitdir:")?.trim();
9573    let gitdir_abs = fs.canonicalize(&path.join(gitdir_rel)).await.ok()?;
9574    // Read commondir to find the main .git directory
9575    let commondir_content = fs.load(&gitdir_abs.join("commondir")).await.ok()?;
9576    let common_dir = fs
9577        .canonicalize(&gitdir_abs.join(commondir_content.trim()))
9578        .await
9579        .ok()?;
9580    Some(repo_identity_path(&common_dir).to_path_buf())
9581}
9582
9583/// Validates that the resolved worktree directory is acceptable:
9584/// - The setting must not be an absolute path.
9585/// - The resolved path must be either a subdirectory of the working
9586///   directory or a subdirectory of its parent (i.e., a sibling).
9587///
9588/// Returns `Ok(resolved_path)` or an error with a user-facing message.
9589pub fn worktrees_directory_for_repo(
9590    repository_anchor_path: &Path,
9591    worktree_directory_setting: &str,
9592    path_style: PathStyle,
9593) -> Result<PathBuf> {
9594    // Check the original setting before trimming, since a path like "///"
9595    // is absolute but becomes "" after stripping trailing separators.
9596    // Also check for leading `/` or `\` explicitly, because on Windows
9597    // `Path::is_absolute()` requires a drive letter — so `/tmp/worktrees`
9598    // would slip through even though it's clearly not a relative path.
9599    if path_style.is_absolute(worktree_directory_setting)
9600        || worktree_directory_setting.starts_with('\\')
9601    {
9602        anyhow::bail!(
9603            "git.worktree_directory must be a relative path, got: {worktree_directory_setting:?}"
9604        );
9605    }
9606
9607    if worktree_directory_setting.is_empty() {
9608        anyhow::bail!("git.worktree_directory must not be empty");
9609    }
9610
9611    let trimmed = worktree_directory_setting.trim_end_matches(['/', '\\']);
9612    if trimmed == ".." {
9613        anyhow::bail!("git.worktree_directory must not be \"..\" (use \"../some-name\" instead)");
9614    }
9615
9616    let joined = path_style.join_path(repository_anchor_path, trimmed)?;
9617    let resolved = if path_style.is_posix() {
9618        joined
9619    } else {
9620        path::normalize_path(&joined)
9621    };
9622    let resolved = if resolved.starts_with(repository_anchor_path) {
9623        resolved
9624    } else if let Some(repo_dir_name) = repository_anchor_path
9625        .file_name()
9626        .and_then(|name| name.to_str())
9627    {
9628        path_style.join_path(&resolved, repo_dir_name)?
9629    } else {
9630        resolved
9631    };
9632
9633    let parent = repository_anchor_path
9634        .parent()
9635        .unwrap_or(repository_anchor_path);
9636
9637    if !resolved.starts_with(parent) {
9638        anyhow::bail!(
9639            "git.worktree_directory resolved to {resolved:?}, which is outside \
9640             the project root and its parent directory. It must resolve to a \
9641             subdirectory of {repository_anchor_path:?} or a sibling of it."
9642        );
9643    }
9644
9645    Ok(resolved)
9646}
9647
9648async fn remove_empty_managed_worktree_ancestors(fs: &dyn Fs, child_path: &Path, base_path: &Path) {
9649    let mut current = child_path;
9650    while let Some(parent) = current.parent() {
9651        if parent == base_path {
9652            break;
9653        }
9654        if !parent.starts_with(base_path) {
9655            break;
9656        }
9657
9658        let result = fs
9659            .remove_dir(
9660                parent,
9661                RemoveOptions {
9662                    recursive: false,
9663                    ignore_if_not_exists: true,
9664                },
9665            )
9666            .await;
9667
9668        match result {
9669            Ok(()) => {
9670                log::info!(
9671                    "Removed empty managed worktree directory: {}",
9672                    parent.display()
9673                );
9674            }
9675            Err(error) => {
9676                log::debug!(
9677                    "Stopped removing managed worktree parent directories at {}: {error}",
9678                    parent.display()
9679                );
9680                break;
9681            }
9682        }
9683
9684        current = parent;
9685    }
9686}
9687
9688/// Returns the repository's identity path given its common Git directory.
9689///
9690/// This is the canonical, on-disk path used for project grouping and as the
9691/// basis for display names. The goal is to return the directory the user
9692/// thinks of as "the project":
9693///
9694/// - If `common_dir`'s last component starts with `.` (e.g. `.git` for a
9695///   normal checkout, or `.bare` for a bare clone), the parent directory is
9696///   returned. Both of these are internal Git directories; the parent is the
9697///   meaningful project root.
9698/// - Otherwise (e.g. `zed.git` for a bare clone), `common_dir` itself is
9699///   returned — it is already a meaningful on-disk path.
9700pub fn repo_identity_path(common_dir: &Path) -> &Path {
9701    let is_dot_entry = common_dir
9702        .file_name()
9703        .is_some_and(|n| n.to_string_lossy().starts_with('.'));
9704    if is_dot_entry {
9705        common_dir.parent().unwrap_or(common_dir)
9706    } else {
9707        common_dir
9708    }
9709}
9710
9711/// Returns a short name for a linked worktree suitable for UI display
9712///
9713/// Uses the main worktree path to come up with a short name that disambiguates
9714/// the linked worktree from the main worktree.
9715pub fn linked_worktree_short_name(
9716    main_worktree_path: &Path,
9717    linked_worktree_path: &Path,
9718) -> Option<SharedString> {
9719    if main_worktree_path == linked_worktree_path {
9720        return None;
9721    }
9722
9723    let project_name = main_worktree_path.file_name()?.to_str()?;
9724    let directory_name = linked_worktree_path.file_name()?.to_str()?;
9725    let name = if directory_name != project_name {
9726        directory_name.to_string()
9727    } else {
9728        linked_worktree_path
9729            .parent()?
9730            .file_name()?
9731            .to_str()?
9732            .to_string()
9733    };
9734    Some(name.into())
9735}
9736
9737fn get_permalink_in_rust_registry_src(
9738    provider_registry: Arc<GitHostingProviderRegistry>,
9739    path: PathBuf,
9740    selection: Range<u32>,
9741) -> Result<url::Url> {
9742    #[derive(Deserialize)]
9743    struct CargoVcsGit {
9744        sha1: String,
9745    }
9746
9747    #[derive(Deserialize)]
9748    struct CargoVcsInfo {
9749        git: CargoVcsGit,
9750        path_in_vcs: String,
9751    }
9752
9753    #[derive(Deserialize)]
9754    struct CargoPackage {
9755        repository: String,
9756    }
9757
9758    #[derive(Deserialize)]
9759    struct CargoToml {
9760        package: CargoPackage,
9761    }
9762
9763    let Some((dir, cargo_vcs_info_json)) = path.ancestors().skip(1).find_map(|dir| {
9764        let json = std::fs::read_to_string(dir.join(".cargo_vcs_info.json")).ok()?;
9765        Some((dir, json))
9766    }) else {
9767        bail!("No .cargo_vcs_info.json found in parent directories")
9768    };
9769    let cargo_vcs_info = serde_json::from_str::<CargoVcsInfo>(&cargo_vcs_info_json)?;
9770    let cargo_toml = std::fs::read_to_string(dir.join("Cargo.toml"))?;
9771    let manifest = toml::from_str::<CargoToml>(&cargo_toml)?;
9772    let (provider, remote) = parse_git_remote_url(provider_registry, &manifest.package.repository)
9773        .context("parsing package.repository field of manifest")?;
9774    let path = PathBuf::from(cargo_vcs_info.path_in_vcs).join(path.strip_prefix(dir).unwrap());
9775    let permalink = provider.build_permalink(
9776        remote,
9777        BuildPermalinkParams::new(
9778            &cargo_vcs_info.git.sha1,
9779            &RepoPath::from_rel_path(
9780                &RelPath::new(&path, PathStyle::local()).context("invalid path")?,
9781            ),
9782            Some(selection),
9783        ),
9784    );
9785    Ok(permalink)
9786}
9787
9788fn serialize_blame_buffer_response(blame: Option<git::blame::Blame>) -> proto::BlameBufferResponse {
9789    let Some(blame) = blame else {
9790        return proto::BlameBufferResponse {
9791            blame_response: None,
9792        };
9793    };
9794
9795    let entries = blame
9796        .entries
9797        .into_iter()
9798        .map(|entry| proto::BlameEntry {
9799            sha: entry.sha.as_bytes().into(),
9800            start_line: entry.range.start,
9801            end_line: entry.range.end,
9802            original_line_number: entry.original_line_number,
9803            author: entry.author,
9804            author_mail: entry.author_mail,
9805            author_time: entry.author_time,
9806            author_tz: entry.author_tz,
9807            committer: entry.committer_name,
9808            committer_mail: entry.committer_email,
9809            committer_time: entry.committer_time,
9810            committer_tz: entry.committer_tz,
9811            summary: entry.summary,
9812            previous: entry.previous,
9813            filename: entry.filename,
9814        })
9815        .collect::<Vec<_>>();
9816
9817    let messages = blame
9818        .messages
9819        .into_iter()
9820        .map(|(oid, message)| proto::CommitMessage {
9821            oid: oid.as_bytes().into(),
9822            message,
9823        })
9824        .collect::<Vec<_>>();
9825
9826    proto::BlameBufferResponse {
9827        blame_response: Some(proto::blame_buffer_response::BlameResponse { entries, messages }),
9828    }
9829}
9830
9831fn deserialize_blame_buffer_response(
9832    response: proto::BlameBufferResponse,
9833) -> Option<git::blame::Blame> {
9834    let response = response.blame_response?;
9835    let entries = response
9836        .entries
9837        .into_iter()
9838        .filter_map(|entry| {
9839            Some(git::blame::BlameEntry {
9840                sha: git::Oid::from_bytes(&entry.sha).ok()?,
9841                range: entry.start_line..entry.end_line,
9842                original_line_number: entry.original_line_number,
9843                committer_name: entry.committer,
9844                committer_time: entry.committer_time,
9845                committer_tz: entry.committer_tz,
9846                committer_email: entry.committer_mail,
9847                author: entry.author,
9848                author_mail: entry.author_mail,
9849                author_time: entry.author_time,
9850                author_tz: entry.author_tz,
9851                summary: entry.summary,
9852                previous: entry.previous,
9853                filename: entry.filename,
9854            })
9855        })
9856        .collect::<Vec<_>>();
9857
9858    let messages = response
9859        .messages
9860        .into_iter()
9861        .filter_map(|message| Some((git::Oid::from_bytes(&message.oid).ok()?, message.message)))
9862        .collect::<HashMap<_, _>>();
9863
9864    Some(Blame {
9865        entries,
9866        messages,
9867        tag_names: Default::default(),
9868    })
9869}
9870
9871fn log_source_to_proto(log_source: &LogSource) -> proto::GitLogSource {
9872    proto::GitLogSource {
9873        source: Some(match log_source {
9874            LogSource::All => proto::git_log_source::Source::All(proto::GitLogSourceAll {}),
9875            LogSource::Branch(branch) => proto::git_log_source::Source::Branch(branch.to_string()),
9876            LogSource::Sha(sha) => proto::git_log_source::Source::Sha(sha.to_string()),
9877            LogSource::Path(path) => {
9878                proto::git_log_source::Source::Path(path.as_unix_str().to_owned())
9879            }
9880        }),
9881    }
9882}
9883
9884fn log_source_from_proto(log_source: proto::GitLogSource) -> Result<LogSource> {
9885    match log_source
9886        .source
9887        .context("git log source is missing source")?
9888    {
9889        proto::git_log_source::Source::All(_) => Ok(LogSource::All),
9890        proto::git_log_source::Source::Branch(branch) => Ok(LogSource::Branch(branch.into())),
9891        proto::git_log_source::Source::Sha(sha) => Ok(LogSource::Sha(Oid::from_str(&sha)?)),
9892        proto::git_log_source::Source::Path(path) => {
9893            Ok(LogSource::Path(RepoPath::from_proto(&path)?))
9894        }
9895    }
9896}
9897
9898fn log_order_to_proto(log_order: LogOrder) -> i32 {
9899    match log_order {
9900        LogOrder::DateOrder => proto::get_initial_graph_data::LogOrder::DateOrder as i32,
9901        LogOrder::TopoOrder => proto::get_initial_graph_data::LogOrder::TopoOrder as i32,
9902        LogOrder::AuthorDateOrder => {
9903            proto::get_initial_graph_data::LogOrder::AuthorDateOrder as i32
9904        }
9905        LogOrder::ReverseChronological => {
9906            proto::get_initial_graph_data::LogOrder::ReverseChronological as i32
9907        }
9908    }
9909}
9910
9911fn log_order_from_proto(log_order: proto::get_initial_graph_data::LogOrder) -> LogOrder {
9912    match log_order {
9913        proto::get_initial_graph_data::LogOrder::DateOrder => LogOrder::DateOrder,
9914        proto::get_initial_graph_data::LogOrder::TopoOrder => LogOrder::TopoOrder,
9915        proto::get_initial_graph_data::LogOrder::AuthorDateOrder => LogOrder::AuthorDateOrder,
9916        proto::get_initial_graph_data::LogOrder::ReverseChronological => {
9917            LogOrder::ReverseChronological
9918        }
9919    }
9920}
9921
9922fn initial_graph_commit_to_proto(commit: &InitialGraphCommitData) -> proto::InitialGraphCommit {
9923    proto::InitialGraphCommit {
9924        sha: commit.sha.to_string(),
9925        parents: commit
9926            .parents
9927            .iter()
9928            .map(|parent| parent.to_string())
9929            .collect(),
9930        ref_names: commit
9931            .ref_names
9932            .iter()
9933            .map(|ref_name| ref_name.to_string())
9934            .collect(),
9935    }
9936}
9937
9938fn initial_graph_commit_from_proto(
9939    commit: proto::InitialGraphCommit,
9940) -> Result<Arc<InitialGraphCommitData>> {
9941    let sha = Oid::from_str(&commit.sha)?;
9942    let mut parents = SmallVec::with_capacity(commit.parents.len());
9943    for parent in &commit.parents {
9944        parents.push(Oid::from_str(parent)?);
9945    }
9946    Ok(Arc::new(InitialGraphCommitData {
9947        sha,
9948        parents,
9949        ref_names: commit
9950            .ref_names
9951            .into_iter()
9952            .map(SharedString::from)
9953            .collect(),
9954    }))
9955}
9956
9957fn commit_data_to_proto(commit: &CommitData) -> proto::CommitData {
9958    proto::CommitData {
9959        sha: commit.sha.to_string(),
9960        parents: commit.parents.iter().map(|p| p.to_string()).collect(),
9961        author_name: commit.author_name.to_string(),
9962        author_email: commit.author_email.to_string(),
9963        commit_timestamp: commit.commit_timestamp,
9964        subject: commit.subject.to_string(),
9965        message: commit.message.to_string(),
9966    }
9967}
9968
9969fn commit_data_from_proto(commit: proto::CommitData) -> Result<CommitData> {
9970    let sha = Oid::from_str(&commit.sha)?;
9971    let mut parents = SmallVec::with_capacity(commit.parents.len());
9972    for parent in &commit.parents {
9973        parents.push(Oid::from_str(parent)?);
9974    }
9975    Ok(CommitData {
9976        sha,
9977        parents,
9978        author_name: SharedString::from(commit.author_name),
9979        author_email: SharedString::from(commit.author_email),
9980        commit_timestamp: commit.commit_timestamp,
9981        subject: SharedString::from(commit.subject),
9982        message: SharedString::from(commit.message),
9983    })
9984}
9985
9986fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch {
9987    proto::Branch {
9988        is_head: branch.is_head,
9989        ref_name: branch.ref_name.to_string(),
9990        unix_timestamp: branch
9991            .most_recent_commit
9992            .as_ref()
9993            .map(|commit| commit.commit_timestamp as u64),
9994        upstream: branch.upstream.as_ref().map(|upstream| proto::GitUpstream {
9995            ref_name: upstream.ref_name.to_string(),
9996            tracking: upstream
9997                .tracking
9998                .status()
9999                .map(|upstream| proto::UpstreamTracking {
10000                    ahead: upstream.ahead as u64,
10001                    behind: upstream.behind as u64,
10002                }),
10003        }),
10004        most_recent_commit: branch
10005            .most_recent_commit
10006            .as_ref()
10007            .map(|commit| proto::CommitSummary {
10008                sha: commit.sha.to_string(),
10009                subject: commit.subject.to_string(),
10010                commit_timestamp: commit.commit_timestamp,
10011                author_name: commit.author_name.to_string(),
10012            }),
10013    }
10014}
10015
10016fn worktree_to_proto(worktree: &git::repository::Worktree) -> proto::Worktree {
10017    proto::Worktree {
10018        path: worktree.path.to_string_lossy().to_string(),
10019        ref_name: worktree
10020            .ref_name
10021            .as_ref()
10022            .map(|s| s.to_string())
10023            .unwrap_or_default(),
10024        sha: worktree.sha.to_string(),
10025        is_main: worktree.is_main,
10026        is_bare: worktree.is_bare,
10027    }
10028}
10029
10030fn proto_to_worktree(proto: &proto::Worktree) -> git::repository::Worktree {
10031    git::repository::Worktree {
10032        path: PathBuf::from(proto.path.clone()),
10033        ref_name: if proto.ref_name.is_empty() {
10034            None
10035        } else {
10036            Some(SharedString::from(&proto.ref_name))
10037        },
10038        sha: proto.sha.clone().into(),
10039        is_main: proto.is_main,
10040        is_bare: proto.is_bare,
10041    }
10042}
10043
10044fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {
10045    git::repository::Branch {
10046        is_head: proto.is_head,
10047        ref_name: proto.ref_name.clone().into(),
10048        upstream: proto
10049            .upstream
10050            .as_ref()
10051            .map(|upstream| git::repository::Upstream {
10052                ref_name: upstream.ref_name.to_string().into(),
10053                tracking: upstream
10054                    .tracking
10055                    .as_ref()
10056                    .map(|tracking| {
10057                        git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus {
10058                            ahead: tracking.ahead as u32,
10059                            behind: tracking.behind as u32,
10060                        })
10061                    })
10062                    .unwrap_or(git::repository::UpstreamTracking::Gone),
10063            }),
10064        most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| {
10065            git::repository::CommitSummary {
10066                sha: commit.sha.to_string().into(),
10067                subject: commit.subject.to_string().into(),
10068                commit_timestamp: commit.commit_timestamp,
10069                author_name: commit.author_name.to_string().into(),
10070                has_parent: true,
10071            }
10072        }),
10073    }
10074}
10075
10076fn commit_details_to_proto(commit: &CommitDetails) -> proto::GitCommitDetails {
10077    proto::GitCommitDetails {
10078        sha: commit.sha.to_string(),
10079        message: commit.message.to_string(),
10080        commit_timestamp: commit.commit_timestamp,
10081        author_email: commit.author_email.to_string(),
10082        author_name: commit.author_name.to_string(),
10083    }
10084}
10085
10086fn proto_to_commit_details(proto: &proto::GitCommitDetails) -> CommitDetails {
10087    CommitDetails {
10088        sha: proto.sha.clone().into(),
10089        message: proto.message.clone().into(),
10090        commit_timestamp: proto.commit_timestamp,
10091        author_email: proto.author_email.clone().into(),
10092        author_name: proto.author_name.clone().into(),
10093    }
10094}
10095
10096async fn append_pattern_to_ignore_file(
10097    fs: Arc<dyn Fs>,
10098    file_path: PathBuf,
10099    pattern: String,
10100) -> Result<()> {
10101    let existing_content = match fs.load(&file_path).await {
10102        Ok(content) => content,
10103        Err(error)
10104            if error
10105                .root_cause()
10106                .downcast_ref::<std::io::Error>()
10107                .is_some_and(|error| error.kind() == std::io::ErrorKind::NotFound) =>
10108        {
10109            String::new()
10110        }
10111        Err(error) => {
10112            return Err(error).with_context(|| format!("loading {}", file_path.display()));
10113        }
10114    };
10115
10116    if existing_content.lines().any(|line| line.trim() == pattern) {
10117        return Ok(());
10118    }
10119
10120    let new_content = if existing_content.is_empty() {
10121        format!("{}\n", pattern)
10122    } else if existing_content.ends_with('\n') {
10123        format!("{}{}\n", existing_content, pattern)
10124    } else {
10125        format!("{}\n{}\n", existing_content, pattern)
10126    };
10127
10128    fs.save(
10129        &file_path,
10130        &text::Rope::from(new_content.as_str()),
10131        text::LineEnding::Unix,
10132    )
10133    .await
10134}
10135
10136#[cfg(any(test, feature = "test-support"))]
10137impl Repository {
10138    pub fn set_branch_list_for_test(&mut self, branches: Vec<Branch>, cx: &mut Context<Self>) {
10139        self.snapshot.branch_list = branches.into();
10140        cx.emit(RepositoryEvent::BranchListChanged);
10141    }
10142
10143    pub fn loaded_commit_data_for_test(&self) -> HashMap<Oid, CommitData> {
10144        self.commit_data
10145            .iter()
10146            .filter_map(|(sha, state)| match state {
10147                CommitDataState::Loaded(data) => Some((*sha, data.as_ref().clone())),
10148                CommitDataState::Loading(_) => None,
10149            })
10150            .collect()
10151    }
10152}
10153
10154#[cfg(test)]
10155mod tests {
10156    use super::*;
10157    use crate::Project;
10158    use fs::{FakeFs, Fs};
10159    use git::repository::{RepoPath, repo_path};
10160    use gpui::TestAppContext;
10161    use gpui::proptest::prelude::*;
10162    use rand::{SeedableRng, rngs::StdRng};
10163    use serde_json::json;
10164    use settings::SettingsStore;
10165    use std::path::{Path, PathBuf};
10166
10167    fn init_test(cx: &mut TestAppContext) {
10168        cx.update(|cx| {
10169            let settings_store = SettingsStore::test(cx);
10170            cx.set_global(settings_store);
10171        });
10172    }
10173
10174    #[gpui::test]
10175    async fn test_open_uncommitted_diff_skips_symlinks(cx: &mut TestAppContext) {
10176        use util::rel_path::rel_path;
10177
10178        init_test(cx);
10179
10180        let fs = FakeFs::new(cx.executor());
10181        fs.insert_tree(
10182            Path::new("/project"),
10183            json!({
10184                ".git": {},
10185                "target.txt": "rule one\nrule two\n",
10186            }),
10187        )
10188        .await;
10189        fs.insert_symlink("/project/agents.md", PathBuf::from("target.txt"))
10190            .await;
10191
10192        fs.set_head_and_index_for_repo(
10193            Path::new("/project/.git"),
10194            &[
10195                // git stores the symlink's target path as the blob for `agents.md`
10196                ("agents.md", "target.txt".into()),
10197                ("target.txt", "rule one\n".into()),
10198            ],
10199        );
10200
10201        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
10202        project
10203            .update(cx, |project, cx| project.git_scans_complete(cx))
10204            .await;
10205
10206        let worktree_id = project.read_with(cx, |project, cx| {
10207            project.worktrees(cx).next().unwrap().read(cx).id()
10208        });
10209
10210        // symlink file should not produce a base diff
10211        let symlink_buffer = project
10212            .update(cx, |project, cx| {
10213                project.open_buffer((worktree_id, rel_path("agents.md")), cx)
10214            })
10215            .await
10216            .unwrap();
10217        let symlink_diff = project
10218            .update(cx, |project, cx| {
10219                project.open_uncommitted_diff(symlink_buffer, cx)
10220            })
10221            .await
10222            .unwrap();
10223        symlink_diff.read_with(cx, |diff, _| {
10224            assert!(
10225                !diff.base_text_exists(),
10226                "symlinked buffer should not have a git diff base"
10227            );
10228        });
10229
10230        // regular file should still produce a base diff
10231        let regular_buffer = project
10232            .update(cx, |project, cx| {
10233                project.open_buffer((worktree_id, rel_path("target.txt")), cx)
10234            })
10235            .await
10236            .unwrap();
10237        let regular_diff = project
10238            .update(cx, |project, cx| {
10239                project.open_uncommitted_diff(regular_buffer, cx)
10240            })
10241            .await
10242            .unwrap();
10243        regular_diff.read_with(cx, |diff, _| {
10244            assert!(
10245                diff.base_text_exists(),
10246                "regular file should have a git diff base"
10247            );
10248        });
10249    }
10250
10251    #[gpui::test]
10252    async fn test_append_pattern_to_ignore_file_creates_and_deduplicates(cx: &mut TestAppContext) {
10253        let fs: Arc<dyn Fs> = FakeFs::new(cx.executor());
10254        let path = PathBuf::from("/root/.gitignore");
10255
10256        // Appending to a non-existent file creates it with a trailing newline.
10257        super::append_pattern_to_ignore_file(fs.clone(), path.clone(), "build/".to_string())
10258            .await
10259            .unwrap();
10260        assert_eq!(fs.load(&path).await.unwrap(), "build/\n");
10261
10262        // Appending the same pattern again is a no-op (deduplication).
10263        super::append_pattern_to_ignore_file(fs.clone(), path.clone(), "build/".to_string())
10264            .await
10265            .unwrap();
10266        assert_eq!(fs.load(&path).await.unwrap(), "build/\n");
10267
10268        // Appending a distinct pattern adds it with a trailing newline.
10269        super::append_pattern_to_ignore_file(fs.clone(), path.clone(), "target/".to_string())
10270            .await
10271            .unwrap();
10272        assert_eq!(fs.load(&path).await.unwrap(), "build/\ntarget/\n");
10273    }
10274
10275    #[gpui::test]
10276    async fn test_append_pattern_adds_newline_before_pattern_when_missing(cx: &mut TestAppContext) {
10277        let fs: Arc<dyn Fs> = FakeFs::new(cx.executor());
10278        let path = PathBuf::from("/root/.gitignore");
10279
10280        // Pre-populate the file without a trailing newline.
10281        fs.save(&path, &text::Rope::from("*.log"), text::LineEnding::Unix)
10282            .await
10283            .unwrap();
10284
10285        // The new pattern must be written on its own line.
10286        super::append_pattern_to_ignore_file(fs.clone(), path.clone(), "build/".to_string())
10287            .await
10288            .unwrap();
10289        assert_eq!(fs.load(&path).await.unwrap(), "*.log\nbuild/\n");
10290    }
10291
10292    #[test]
10293    fn test_new_worktree_path_uses_posix_style_for_remote_paths() {
10294        let work_dir = Path::new("/home/user/dev/lsp-tests");
10295        let directory =
10296            worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Unix).unwrap();
10297        let directory = PathStyle::Unix.join_path(&directory, "nimble-sky").unwrap();
10298        let path = PathStyle::Unix.join_path(&directory, "lsp-tests").unwrap();
10299
10300        assert_eq!(
10301            path,
10302            PathBuf::from("/home/user/dev/worktrees/lsp-tests/nimble-sky/lsp-tests")
10303        );
10304    }
10305
10306    fn verify_invariants(repository: &Repository) -> anyhow::Result<()> {
10307        match &repository.commit_data_handler {
10308            CommitDataHandlerState::Open(handler) => {
10309                verify_loading_entries_are_pending(repository, handler)?;
10310                verify_await_result_loading_entries_have_completion_senders(repository, handler)?;
10311                verify_pending_requests_are_loading(repository, handler)?;
10312                verify_completion_senders_are_await_result_loading(repository, handler)?;
10313                verify_completion_senders_are_pending(handler)?;
10314                verify_non_await_result_loading_entries_have_no_completion_sender(
10315                    repository, handler,
10316                )?;
10317                verify_loaded_entries_are_not_pending(repository, handler)?;
10318                verify_loaded_entries_have_no_completion_sender(repository, handler)?;
10319            }
10320            CommitDataHandlerState::Closed => {
10321                verify_closed_handler_invariants(repository)?;
10322            }
10323        }
10324
10325        Ok(())
10326    }
10327
10328    fn verify_loading_entries_are_pending(
10329        repository: &Repository,
10330        handler: &CommitDataHandler,
10331    ) -> anyhow::Result<()> {
10332        for (sha, state) in &repository.commit_data {
10333            if matches!(state, CommitDataState::Loading(_)) {
10334                anyhow::ensure!(
10335                    handler.pending_requests.contains(sha),
10336                    "loading commit data for {sha} must be tracked in pending_requests"
10337                );
10338            }
10339        }
10340
10341        Ok(())
10342    }
10343
10344    fn verify_await_result_loading_entries_have_completion_senders(
10345        repository: &Repository,
10346        handler: &CommitDataHandler,
10347    ) -> anyhow::Result<()> {
10348        for (sha, state) in &repository.commit_data {
10349            if matches!(state, CommitDataState::Loading(Some(_))) {
10350                anyhow::ensure!(
10351                    handler.completion_senders.contains_key(sha),
10352                    "await-result loading commit data for {sha} must have a completion sender"
10353                );
10354            }
10355        }
10356
10357        Ok(())
10358    }
10359
10360    fn verify_pending_requests_are_loading(
10361        repository: &Repository,
10362        handler: &CommitDataHandler,
10363    ) -> anyhow::Result<()> {
10364        for sha in &handler.pending_requests {
10365            anyhow::ensure!(
10366                matches!(
10367                    repository.commit_data.get(sha),
10368                    Some(CommitDataState::Loading(_))
10369                ),
10370                "pending request for {sha} must correspond to loading commit data"
10371            );
10372        }
10373
10374        Ok(())
10375    }
10376
10377    fn verify_completion_senders_are_await_result_loading(
10378        repository: &Repository,
10379        handler: &CommitDataHandler,
10380    ) -> anyhow::Result<()> {
10381        for sha in handler.completion_senders.keys() {
10382            anyhow::ensure!(
10383                matches!(
10384                    repository.commit_data.get(sha),
10385                    Some(CommitDataState::Loading(Some(_)))
10386                ),
10387                "completion sender for {sha} must correspond to await-result loading commit data"
10388            );
10389        }
10390
10391        Ok(())
10392    }
10393
10394    fn verify_completion_senders_are_pending(handler: &CommitDataHandler) -> anyhow::Result<()> {
10395        for sha in handler.completion_senders.keys() {
10396            anyhow::ensure!(
10397                handler.pending_requests.contains(sha),
10398                "completion sender for {sha} must also be tracked as pending"
10399            );
10400        }
10401
10402        Ok(())
10403    }
10404
10405    fn verify_non_await_result_loading_entries_have_no_completion_sender(
10406        repository: &Repository,
10407        handler: &CommitDataHandler,
10408    ) -> anyhow::Result<()> {
10409        for (sha, state) in &repository.commit_data {
10410            if matches!(state, CommitDataState::Loading(None)) {
10411                anyhow::ensure!(
10412                    !handler.completion_senders.contains_key(sha),
10413                    "non-await-result loading commit data for {sha} must not have a completion sender"
10414                );
10415            }
10416        }
10417
10418        Ok(())
10419    }
10420
10421    fn verify_loaded_entries_are_not_pending(
10422        repository: &Repository,
10423        handler: &CommitDataHandler,
10424    ) -> anyhow::Result<()> {
10425        for (sha, state) in &repository.commit_data {
10426            if matches!(state, CommitDataState::Loaded(_)) {
10427                anyhow::ensure!(
10428                    !handler.pending_requests.contains(sha),
10429                    "loaded commit data for {sha} must not still be pending"
10430                );
10431            }
10432        }
10433
10434        Ok(())
10435    }
10436
10437    fn verify_loaded_entries_have_no_completion_sender(
10438        repository: &Repository,
10439        handler: &CommitDataHandler,
10440    ) -> anyhow::Result<()> {
10441        for (sha, state) in &repository.commit_data {
10442            if matches!(state, CommitDataState::Loaded(_)) {
10443                anyhow::ensure!(
10444                    !handler.completion_senders.contains_key(sha),
10445                    "loaded commit data for {sha} must not keep a completion sender"
10446                );
10447            }
10448        }
10449
10450        Ok(())
10451    }
10452
10453    fn verify_closed_handler_invariants(repository: &Repository) -> anyhow::Result<()> {
10454        for (sha, state) in &repository.commit_data {
10455            anyhow::ensure!(
10456                !matches!(state, CommitDataState::Loading(_)),
10457                "closed handler must not keep loading commit data for {sha}"
10458            );
10459        }
10460
10461        Ok(())
10462    }
10463
10464    #[gpui::property_test(config = ProptestConfig {
10465        cases: 20,
10466        ..Default::default()
10467    })]
10468    async fn test_commit_data_random_invariants(
10469        #[strategy = any::<u64>()] seed: u64,
10470        #[strategy = gpui::proptest::collection::vec(0usize..2000, 1..200)] commit_indexes: Vec<
10471            usize,
10472        >,
10473        #[strategy = gpui::proptest::collection::vec(any::<bool>(), 1..200)] await_results: Vec<
10474            bool,
10475        >,
10476        #[strategy = gpui::proptest::collection::vec(0usize..2000, 0..200)] failing_commit_indexes: Vec<
10477            usize,
10478        >,
10479        #[strategy = gpui::proptest::collection::vec(0usize..2000, 0..200)] missing_commit_indexes: Vec<
10480            usize,
10481        >,
10482        cx: &mut TestAppContext,
10483    ) {
10484        init_test(cx);
10485        let mut rng = StdRng::seed_from_u64(seed);
10486
10487        let commit_shas = (0..2000).map(|_| Oid::random(&mut rng)).collect::<Vec<_>>();
10488        let failing_shas = failing_commit_indexes
10489            .into_iter()
10490            .map(|index| commit_shas[index % commit_shas.len()])
10491            .collect::<HashSet<_>>();
10492        let missing_shas = missing_commit_indexes
10493            .into_iter()
10494            .map(|index| commit_shas[index % commit_shas.len()])
10495            .collect::<HashSet<_>>();
10496        let commit_data = commit_shas
10497            .iter()
10498            .filter(|sha| !missing_shas.contains(sha))
10499            .map(|sha| {
10500                (
10501                    CommitData {
10502                        sha: *sha,
10503                        parents: SmallVec::new(),
10504                        author_name: SharedString::from(format!("Author {sha}")),
10505                        author_email: SharedString::from(format!("{sha}@example.com")),
10506                        commit_timestamp: rng.random_range(0..10_000),
10507                        subject: SharedString::from(format!("Subject {sha}")),
10508                        message: SharedString::from(format!("Subject {sha}\n\nBody for {sha}")),
10509                    },
10510                    failing_shas.contains(sha),
10511                )
10512            })
10513            .collect::<Vec<_>>();
10514        let expected_loaded_shas = commit_indexes
10515            .iter()
10516            .map(|index| commit_shas[index % commit_shas.len()])
10517            .filter(|sha| !failing_shas.contains(sha) && !missing_shas.contains(sha))
10518            .collect::<HashSet<_>>();
10519
10520        let fs = FakeFs::new(cx.executor());
10521        fs.insert_tree(
10522            Path::new("/project"),
10523            json!({
10524                ".git": {},
10525                "file.txt": "content",
10526            }),
10527        )
10528        .await;
10529        fs.set_commit_data(Path::new("/project/.git"), commit_data);
10530
10531        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
10532        project
10533            .update(cx, |project, cx| project.git_scans_complete(cx))
10534            .await;
10535
10536        let repository = project.read_with(cx, |project, cx| {
10537            project
10538                .active_repository(cx)
10539                .expect("should have a repository")
10540        });
10541
10542        cx.update(|cx| {
10543            cx.observe(&repository, |repo, cx| {
10544                verify_invariants(repo.read(cx))
10545                    .context("Invariant weren't held after a cx.notify")
10546                    .unwrap();
10547            })
10548        })
10549        .detach();
10550
10551        let mut next_step = 0;
10552        while next_step < commit_indexes.len() {
10553            let remaining_steps = commit_indexes.len() - next_step;
10554            let chunk_size = rng.random_range(1..=remaining_steps.min(16));
10555            let chunk_end = next_step + chunk_size;
10556
10557            for step in next_step..chunk_end {
10558                let sha = commit_shas[commit_indexes[step] % commit_shas.len()];
10559                let await_result = await_results[step % await_results.len()];
10560
10561                repository.update(cx, |repository, cx| {
10562                    repository.fetch_commit_data(sha, await_result, cx);
10563                    verify_invariants(repository)
10564                        .with_context(|| {
10565                            format!(
10566                                "commit data invariant violation after step {} for sha {}",
10567                                step + 1,
10568                                sha,
10569                            )
10570                        })
10571                        .unwrap();
10572                });
10573            }
10574
10575            cx.run_until_parked();
10576            repository.read_with(cx, |repository, _cx| {
10577                verify_invariants(repository)
10578                    .with_context(|| {
10579                        format!(
10580                            "commit data invariant violation after draining through step {}",
10581                            chunk_end,
10582                        )
10583                    })
10584                    .unwrap();
10585            });
10586
10587            next_step = chunk_end;
10588        }
10589
10590        cx.run_until_parked();
10591        repository.read_with(cx, |repository, _cx| {
10592            verify_invariants(repository)
10593                .with_context(|| "commit data invariant violation after final drain".to_string())
10594                .unwrap();
10595
10596            let loaded_shas = repository
10597                .commit_data
10598                .iter()
10599                .filter_map(|(sha, state)| match state {
10600                    CommitDataState::Loaded(_) => Some(*sha),
10601                    CommitDataState::Loading(_) => None,
10602                })
10603                .collect::<HashSet<_>>();
10604            let missing_loaded_shas = expected_loaded_shas
10605                .difference(&loaded_shas)
10606                .copied()
10607                .collect::<Vec<_>>();
10608            let unexpected_loaded_shas = loaded_shas
10609                .difference(&expected_loaded_shas)
10610                .copied()
10611                .collect::<Vec<_>>();
10612            assert!(
10613                missing_loaded_shas.is_empty() && unexpected_loaded_shas.is_empty(),
10614                "loaded commit data SHAs after final drain did not match expectation. missing: {:?}, unexpected: {:?}",
10615                missing_loaded_shas,
10616                unexpected_loaded_shas,
10617            );
10618        });
10619    }
10620
10621    fn repo_paths(paths: &[&str]) -> Vec<RepoPath> {
10622        paths.iter().map(repo_path).collect()
10623    }
10624
10625    #[test]
10626    fn coalesce_repo_paths_keeps_root_only() {
10627        let coalesced = GitStore::coalesce_repo_paths(repo_paths(&["", "src", "src/lib.rs"]));
10628
10629        assert_eq!(coalesced, repo_paths(&[""]));
10630    }
10631
10632    #[test]
10633    fn coalesce_repo_paths_keeps_existing_ancestors() {
10634        let coalesced = GitStore::coalesce_repo_paths(repo_paths(&[
10635            "src",
10636            "src/lib.rs",
10637            "src/nested/file.rs",
10638            "tests/test.rs",
10639        ]));
10640
10641        assert_eq!(coalesced, repo_paths(&["src", "tests/test.rs"]));
10642    }
10643
10644    #[test]
10645    fn coalesce_repo_paths_does_not_invent_missing_parents() {
10646        let coalesced = GitStore::coalesce_repo_paths(repo_paths(&[
10647            "submodule/a.txt",
10648            "submodule/nested/b.txt",
10649            "top_level.rs",
10650        ]));
10651
10652        assert_eq!(
10653            coalesced,
10654            repo_paths(&["submodule/a.txt", "submodule/nested/b.txt", "top_level.rs"])
10655        );
10656    }
10657}
10658
10659/// This snapshot computes the repository state on the foreground thread while
10660/// running the git commands on the background thread. We update branch, head,
10661/// remotes, and worktrees first so the UI can react sooner, then compute file
10662/// state and emit those events immediately after.
10663async fn compute_snapshot(
10664    this: Entity<Repository>,
10665    backend: Arc<dyn GitRepository>,
10666    cx: &mut AsyncApp,
10667) -> RepositorySnapshot {
10668    log::debug!("starting compute snapshot");
10669
10670    let (id, work_directory_abs_path, prev_snapshot) = this.update(cx, |this, _| {
10671        this.paths_needing_status_update.clear();
10672        (
10673            this.id,
10674            this.work_directory_abs_path.clone(),
10675            this.snapshot.clone(),
10676        )
10677    });
10678
10679    let branches_future = {
10680        let backend = backend.clone();
10681        async move { backend.branches().await.log_err().unwrap_or_default() }
10682    };
10683    let head_commit_future = {
10684        let backend = backend.clone();
10685        async move { backend.show("HEAD".to_string()).await.ok() }
10686    };
10687    let worktrees_future = {
10688        let backend = backend.clone();
10689        async move { backend.worktrees().await.log_err().unwrap_or_default() }
10690    };
10691    let (branches, head_commit, all_worktrees) =
10692        futures::future::join3(branches_future, head_commit_future, worktrees_future).await;
10693    log::debug!("fetched branches, head commit, worktrees");
10694
10695    let BranchesScanResult {
10696        branches,
10697        error: branch_list_error,
10698    } = branches;
10699    let branch = branches.iter().find(|branch| branch.is_head).cloned();
10700    let branch_list: Arc<[Branch]> = branches.into();
10701
10702    let linked_worktrees: Arc<[GitWorktree]> = all_worktrees
10703        .into_iter()
10704        .filter(|wt| wt.path != *work_directory_abs_path)
10705        .collect();
10706
10707    let mut remote_urls = backend.remote_urls().await;
10708    let remote_origin_url = remote_urls.remove("origin");
10709    let remote_upstream_url = remote_urls.remove("upstream");
10710
10711    log::debug!("fetched remotes");
10712
10713    let snapshot = this.update(cx, |this, cx| {
10714        let head_changed =
10715            branch != this.snapshot.branch || head_commit != this.snapshot.head_commit;
10716        let branch_list_changed = *branch_list != *this.snapshot.branch_list;
10717        let branch_list_error_changed = branch_list_error != this.snapshot.branch_list_error;
10718        let worktrees_changed = *linked_worktrees != *this.snapshot.linked_worktrees;
10719
10720        this.snapshot = RepositorySnapshot {
10721            id,
10722            work_directory_abs_path,
10723            branch,
10724            branch_list: branch_list.clone(),
10725            branch_list_error,
10726            head_commit,
10727            remote_origin_url,
10728            remote_upstream_url,
10729            linked_worktrees,
10730            scan_id: prev_snapshot.scan_id + 1,
10731            ..prev_snapshot
10732        };
10733
10734        if head_changed {
10735            cx.emit(RepositoryEvent::HeadChanged);
10736        }
10737
10738        if branch_list_changed || branch_list_error_changed {
10739            cx.emit(RepositoryEvent::BranchListChanged);
10740        }
10741
10742        if worktrees_changed {
10743            cx.emit(RepositoryEvent::GitWorktreeListChanged);
10744        }
10745
10746        this.snapshot.clone()
10747    });
10748
10749    let statuses_future = {
10750        let backend = backend.clone();
10751        async move {
10752            backend
10753                .status(&[RepoPath::from_rel_path(
10754                    &RelPath::new(".".as_ref(), PathStyle::local()).unwrap(),
10755                )])
10756                .await
10757                .log_err()
10758                .unwrap_or_default()
10759        }
10760    };
10761    let diff_stats_future = {
10762        let snapshot = snapshot.clone();
10763        let backend = backend.clone();
10764        async move {
10765            if snapshot.head_commit.is_some() {
10766                futures::future::join3(
10767                    backend.diff_stat(DiffStatType::HeadToWorktree, &[]),
10768                    backend.diff_stat(DiffStatType::HeadToIndex, &[]),
10769                    backend.diff_stat(DiffStatType::IndexToWorktree, &[]),
10770                )
10771                .await
10772            } else {
10773                (
10774                    Ok(status::GitDiffStat::default()),
10775                    Ok(status::GitDiffStat::default()),
10776                    Ok(status::GitDiffStat::default()),
10777                )
10778            }
10779        }
10780    };
10781    let stash_entries_future = {
10782        let backend = backend.clone();
10783        async move { backend.stash_entries().await.log_err().unwrap_or_default() }
10784    };
10785
10786    let (statuses, diff_stats, stash_entries) =
10787        futures::future::join3(statuses_future, diff_stats_future, stash_entries_future).await;
10788    let (diff_stats, staged_diff_stats, unstaged_diff_stats) = diff_stats;
10789    let diff_stats = diff_stats.log_err().unwrap_or_default();
10790    let staged_diff_stats = staged_diff_stats.log_err().unwrap_or_default();
10791    let unstaged_diff_stats = unstaged_diff_stats.log_err().unwrap_or_default();
10792    log::debug!("fetched statuses, diff stats, stash entries");
10793
10794    let diff_stat_map: HashMap<&RepoPath, DiffStat> =
10795        diff_stats.entries.iter().map(|(p, s)| (p, *s)).collect();
10796    let staged_diff_stat_map: HashMap<&RepoPath, DiffStat> = staged_diff_stats
10797        .entries
10798        .iter()
10799        .map(|(p, s)| (p, *s))
10800        .collect();
10801    let unstaged_diff_stat_map: HashMap<&RepoPath, DiffStat> = unstaged_diff_stats
10802        .entries
10803        .iter()
10804        .map(|(p, s)| (p, *s))
10805        .collect();
10806    let mut conflicted_paths = Vec::new();
10807    let statuses_by_path = SumTree::from_iter(
10808        statuses.entries.iter().map(|(repo_path, status)| {
10809            if status.is_conflicted() {
10810                conflicted_paths.push(repo_path.clone());
10811            }
10812            StatusEntry {
10813                repo_path: repo_path.clone(),
10814                status: *status,
10815                diff_stat: diff_stat_map.get(repo_path).copied(),
10816                staged_diff_stat: staged_diff_stat_map.get(repo_path).copied(),
10817                unstaged_diff_stat: unstaged_diff_stat_map.get(repo_path).copied(),
10818            }
10819        }),
10820        (),
10821    );
10822
10823    let (merge_details, conflicts_changed) = cx
10824        .background_spawn({
10825            let backend = backend.clone();
10826            let mut merge_details = snapshot.merge.clone();
10827            async move {
10828                let conflicts_changed = merge_details.update(&backend, conflicted_paths).await;
10829                (merge_details, conflicts_changed)
10830            }
10831        })
10832        .await;
10833    log::debug!("new merge details: {merge_details:?}");
10834
10835    this.update(cx, |this, cx| {
10836        if conflicts_changed || statuses_by_path != this.snapshot.statuses_by_path {
10837            cx.emit(RepositoryEvent::StatusesChanged);
10838        }
10839        if stash_entries != this.snapshot.stash_entries {
10840            cx.emit(RepositoryEvent::StashEntriesChanged);
10841        }
10842
10843        this.snapshot.scan_id += 1;
10844        this.snapshot.merge = merge_details;
10845        this.snapshot.statuses_by_path = statuses_by_path;
10846        this.snapshot.stash_entries = stash_entries;
10847
10848        this.snapshot.clone()
10849    })
10850}
10851
10852fn status_from_proto(
10853    simple_status: i32,
10854    status: Option<proto::GitFileStatus>,
10855) -> anyhow::Result<FileStatus> {
10856    use proto::git_file_status::Variant;
10857
10858    let Some(variant) = status.and_then(|status| status.variant) else {
10859        let code = proto::GitStatus::from_i32(simple_status)
10860            .with_context(|| format!("Invalid git status code: {simple_status}"))?;
10861        let result = match code {
10862            proto::GitStatus::Added => TrackedStatus {
10863                worktree_status: StatusCode::Added,
10864                index_status: StatusCode::Unmodified,
10865            }
10866            .into(),
10867            proto::GitStatus::Modified => TrackedStatus {
10868                worktree_status: StatusCode::Modified,
10869                index_status: StatusCode::Unmodified,
10870            }
10871            .into(),
10872            proto::GitStatus::Conflict => UnmergedStatus {
10873                first_head: UnmergedStatusCode::Updated,
10874                second_head: UnmergedStatusCode::Updated,
10875            }
10876            .into(),
10877            proto::GitStatus::Deleted => TrackedStatus {
10878                worktree_status: StatusCode::Deleted,
10879                index_status: StatusCode::Unmodified,
10880            }
10881            .into(),
10882            _ => anyhow::bail!("Invalid code for simple status: {simple_status}"),
10883        };
10884        return Ok(result);
10885    };
10886
10887    let result = match variant {
10888        Variant::Untracked(_) => FileStatus::Untracked,
10889        Variant::Ignored(_) => FileStatus::Ignored,
10890        Variant::Unmerged(unmerged) => {
10891            let [first_head, second_head] =
10892                [unmerged.first_head, unmerged.second_head].map(|head| {
10893                    let code = proto::GitStatus::from_i32(head)
10894                        .with_context(|| format!("Invalid git status code: {head}"))?;
10895                    let result = match code {
10896                        proto::GitStatus::Added => UnmergedStatusCode::Added,
10897                        proto::GitStatus::Updated => UnmergedStatusCode::Updated,
10898                        proto::GitStatus::Deleted => UnmergedStatusCode::Deleted,
10899                        _ => anyhow::bail!("Invalid code for unmerged status: {code:?}"),
10900                    };
10901                    Ok(result)
10902                });
10903            let [first_head, second_head] = [first_head?, second_head?];
10904            UnmergedStatus {
10905                first_head,
10906                second_head,
10907            }
10908            .into()
10909        }
10910        Variant::Tracked(tracked) => {
10911            let [index_status, worktree_status] = [tracked.index_status, tracked.worktree_status]
10912                .map(|status| {
10913                    let code = proto::GitStatus::from_i32(status)
10914                        .with_context(|| format!("Invalid git status code: {status}"))?;
10915                    let result = match code {
10916                        proto::GitStatus::Modified => StatusCode::Modified,
10917                        proto::GitStatus::TypeChanged => StatusCode::TypeChanged,
10918                        proto::GitStatus::Added => StatusCode::Added,
10919                        proto::GitStatus::Deleted => StatusCode::Deleted,
10920                        proto::GitStatus::Renamed => StatusCode::Renamed,
10921                        proto::GitStatus::Copied => StatusCode::Copied,
10922                        proto::GitStatus::Unmodified => StatusCode::Unmodified,
10923                        _ => anyhow::bail!("Invalid code for tracked status: {code:?}"),
10924                    };
10925                    Ok(result)
10926                });
10927            let [index_status, worktree_status] = [index_status?, worktree_status?];
10928            TrackedStatus {
10929                index_status,
10930                worktree_status,
10931            }
10932            .into()
10933        }
10934    };
10935    Ok(result)
10936}
10937
10938fn status_to_proto(status: FileStatus) -> proto::GitFileStatus {
10939    use proto::git_file_status::{Tracked, Unmerged, Variant};
10940
10941    let variant = match status {
10942        FileStatus::Untracked => Variant::Untracked(Default::default()),
10943        FileStatus::Ignored => Variant::Ignored(Default::default()),
10944        FileStatus::Unmerged(UnmergedStatus {
10945            first_head,
10946            second_head,
10947        }) => Variant::Unmerged(Unmerged {
10948            first_head: unmerged_status_to_proto(first_head),
10949            second_head: unmerged_status_to_proto(second_head),
10950        }),
10951        FileStatus::Tracked(TrackedStatus {
10952            index_status,
10953            worktree_status,
10954        }) => Variant::Tracked(Tracked {
10955            index_status: tracked_status_to_proto(index_status),
10956            worktree_status: tracked_status_to_proto(worktree_status),
10957        }),
10958    };
10959    proto::GitFileStatus {
10960        variant: Some(variant),
10961    }
10962}
10963
10964fn unmerged_status_to_proto(code: UnmergedStatusCode) -> i32 {
10965    match code {
10966        UnmergedStatusCode::Added => proto::GitStatus::Added as _,
10967        UnmergedStatusCode::Deleted => proto::GitStatus::Deleted as _,
10968        UnmergedStatusCode::Updated => proto::GitStatus::Updated as _,
10969    }
10970}
10971
10972fn tracked_status_to_proto(code: StatusCode) -> i32 {
10973    match code {
10974        StatusCode::Added => proto::GitStatus::Added as _,
10975        StatusCode::Deleted => proto::GitStatus::Deleted as _,
10976        StatusCode::Modified => proto::GitStatus::Modified as _,
10977        StatusCode::Renamed => proto::GitStatus::Renamed as _,
10978        StatusCode::TypeChanged => proto::GitStatus::TypeChanged as _,
10979        StatusCode::Copied => proto::GitStatus::Copied as _,
10980        StatusCode::Unmodified => proto::GitStatus::Unmodified as _,
10981    }
10982}
10983
Served at tenant.openagents/omega Member data and write actions are omitted.