git_store.rs

   1pub mod branch_diff;
   2mod conflict_set;
   3pub mod git_traversal;
   4pub mod pending_op;
   5
   6use crate::{
   7    ProjectEnvironment, ProjectItem, ProjectPath,
   8    buffer_store::{BufferStore, BufferStoreEvent},
   9    trusted_worktrees::{
  10        PathTrust, TrustedWorktrees, TrustedWorktreesEvent, TrustedWorktreesStore,
  11    },
  12    worktree_store::{WorktreeStore, WorktreeStoreEvent},
  13};
  14use anyhow::{Context as _, Result, anyhow, bail};
  15use askpass::{AskPassDelegate, EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
  16use buffer_diff::{BufferDiff, BufferDiffEvent};
  17use client::ProjectId;
  18use collections::HashMap;
  19pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate};
  20use fs::Fs;
  21use futures::{
  22    FutureExt, StreamExt,
  23    channel::{
  24        mpsc,
  25        oneshot::{self, Canceled},
  26    },
  27    future::{self, BoxFuture, Shared},
  28    stream::FuturesOrdered,
  29};
  30use git::{
  31    BuildPermalinkParams, GitHostingProviderRegistry, Oid, RunHook,
  32    blame::Blame,
  33    parse_git_remote_url,
  34    repository::{
  35        Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, DiffType, FetchOptions,
  36        GitRepository, GitRepositoryCheckpoint, GraphCommitData, InitialGraphCommitData, LogOrder,
  37        LogSource, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode, SearchCommitArgs,
  38        UpstreamTrackingStatus, Worktree as GitWorktree,
  39    },
  40    stash::{GitStash, StashEntry},
  41    status::{
  42        self, DiffStat, DiffTreeType, FileStatus, GitSummary, StatusCode, TrackedStatus, TreeDiff,
  43        TreeDiffStatus, UnmergedStatus, UnmergedStatusCode,
  44    },
  45};
  46use gpui::{
  47    App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
  48    WeakEntity,
  49};
  50use language::{
  51    Buffer, BufferEvent, Language, LanguageRegistry,
  52    proto::{deserialize_version, serialize_version},
  53};
  54use parking_lot::Mutex;
  55use pending_op::{PendingOp, PendingOpId, PendingOps, PendingOpsSummary};
  56use postage::stream::Stream as _;
  57use rpc::{
  58    AnyProtoClient, TypedEnvelope,
  59    proto::{self, git_reset, split_repository_update},
  60};
  61use serde::Deserialize;
  62use settings::WorktreeId;
  63use smol::future::yield_now;
  64use std::{
  65    cmp::Ordering,
  66    collections::{BTreeSet, HashSet, VecDeque, hash_map::Entry},
  67    future::Future,
  68    mem,
  69    ops::Range,
  70    path::{Path, PathBuf},
  71    str::FromStr,
  72    sync::{
  73        Arc,
  74        atomic::{self, AtomicU64},
  75    },
  76    time::Instant,
  77};
  78use sum_tree::{Edit, SumTree, TreeMap};
  79use task::Shell;
  80use text::{Bias, BufferId};
  81use util::{
  82    ResultExt, debug_panic,
  83    paths::{PathStyle, SanitizedPath},
  84    post_inc,
  85    rel_path::RelPath,
  86};
  87use worktree::{
  88    File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId,
  89    UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree,
  90};
  91use zeroize::Zeroize;
  92
  93pub struct GitStore {
  94    state: GitStoreState,
  95    buffer_store: Entity<BufferStore>,
  96    worktree_store: Entity<WorktreeStore>,
  97    repositories: HashMap<RepositoryId, Entity<Repository>>,
  98    worktree_ids: HashMap<RepositoryId, HashSet<WorktreeId>>,
  99    active_repo_id: Option<RepositoryId>,
 100    #[allow(clippy::type_complexity)]
 101    loading_diffs:
 102        HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
 103    diffs: HashMap<BufferId, Entity<BufferGitState>>,
 104    shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
 105    _subscriptions: Vec<Subscription>,
 106}
 107
 108#[derive(Default)]
 109struct SharedDiffs {
 110    unstaged: Option<Entity<BufferDiff>>,
 111    uncommitted: Option<Entity<BufferDiff>>,
 112}
 113
 114struct BufferGitState {
 115    unstaged_diff: Option<WeakEntity<BufferDiff>>,
 116    uncommitted_diff: Option<WeakEntity<BufferDiff>>,
 117    oid_diffs: HashMap<Option<git::Oid>, WeakEntity<BufferDiff>>,
 118    conflict_set: Option<WeakEntity<ConflictSet>>,
 119    recalculate_diff_task: Option<Task<Result<()>>>,
 120    reparse_conflict_markers_task: Option<Task<Result<()>>>,
 121    language: Option<Arc<Language>>,
 122    language_registry: Option<Arc<LanguageRegistry>>,
 123    conflict_updated_futures: Vec<oneshot::Sender<()>>,
 124    recalculating_tx: postage::watch::Sender<bool>,
 125
 126    /// These operation counts are used to ensure that head and index text
 127    /// values read from the git repository are up-to-date with any hunk staging
 128    /// operations that have been performed on the BufferDiff.
 129    ///
 130    /// The operation count is incremented immediately when the user initiates a
 131    /// hunk stage/unstage operation. Then, upon finishing writing the new index
 132    /// text do disk, the `operation count as of write` is updated to reflect
 133    /// the operation count that prompted the write.
 134    hunk_staging_operation_count: usize,
 135    hunk_staging_operation_count_as_of_write: usize,
 136
 137    head_text: Option<Arc<str>>,
 138    index_text: Option<Arc<str>>,
 139    oid_texts: HashMap<git::Oid, Arc<str>>,
 140    head_changed: bool,
 141    index_changed: bool,
 142    language_changed: bool,
 143}
 144
 145#[derive(Clone, Debug)]
 146enum DiffBasesChange {
 147    SetIndex(Option<String>),
 148    SetHead(Option<String>),
 149    SetEach {
 150        index: Option<String>,
 151        head: Option<String>,
 152    },
 153    SetBoth(Option<String>),
 154}
 155
 156#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 157enum DiffKind {
 158    Unstaged,
 159    Uncommitted,
 160    SinceOid(Option<git::Oid>),
 161}
 162
 163enum GitStoreState {
 164    Local {
 165        next_repository_id: Arc<AtomicU64>,
 166        downstream: Option<LocalDownstreamState>,
 167        project_environment: Entity<ProjectEnvironment>,
 168        fs: Arc<dyn Fs>,
 169    },
 170    Remote {
 171        upstream_client: AnyProtoClient,
 172        upstream_project_id: u64,
 173        downstream: Option<(AnyProtoClient, ProjectId)>,
 174    },
 175}
 176
 177enum DownstreamUpdate {
 178    UpdateRepository(RepositorySnapshot),
 179    RemoveRepository(RepositoryId),
 180}
 181
 182struct LocalDownstreamState {
 183    client: AnyProtoClient,
 184    project_id: ProjectId,
 185    updates_tx: mpsc::UnboundedSender<DownstreamUpdate>,
 186    _task: Task<Result<()>>,
 187}
 188
 189#[derive(Clone, Debug)]
 190pub struct GitStoreCheckpoint {
 191    checkpoints_by_work_dir_abs_path: HashMap<Arc<Path>, GitRepositoryCheckpoint>,
 192}
 193
 194#[derive(Clone, Debug, PartialEq, Eq)]
 195pub struct StatusEntry {
 196    pub repo_path: RepoPath,
 197    pub status: FileStatus,
 198    pub diff_stat: Option<DiffStat>,
 199}
 200
 201impl StatusEntry {
 202    fn to_proto(&self) -> proto::StatusEntry {
 203        let simple_status = match self.status {
 204            FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32,
 205            FileStatus::Unmerged { .. } => proto::GitStatus::Conflict as i32,
 206            FileStatus::Tracked(TrackedStatus {
 207                index_status,
 208                worktree_status,
 209            }) => tracked_status_to_proto(if worktree_status != StatusCode::Unmodified {
 210                worktree_status
 211            } else {
 212                index_status
 213            }),
 214        };
 215
 216        proto::StatusEntry {
 217            repo_path: self.repo_path.to_proto(),
 218            simple_status,
 219            status: Some(status_to_proto(self.status)),
 220            diff_stat_added: self.diff_stat.map(|ds| ds.added),
 221            diff_stat_deleted: self.diff_stat.map(|ds| ds.deleted),
 222        }
 223    }
 224}
 225
 226impl TryFrom<proto::StatusEntry> for StatusEntry {
 227    type Error = anyhow::Error;
 228
 229    fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
 230        let repo_path = RepoPath::from_proto(&value.repo_path).context("invalid repo path")?;
 231        let status = status_from_proto(value.simple_status, value.status)?;
 232        let diff_stat = match (value.diff_stat_added, value.diff_stat_deleted) {
 233            (Some(added), Some(deleted)) => Some(DiffStat { added, deleted }),
 234            _ => None,
 235        };
 236        Ok(Self {
 237            repo_path,
 238            status,
 239            diff_stat,
 240        })
 241    }
 242}
 243
 244impl sum_tree::Item for StatusEntry {
 245    type Summary = PathSummary<GitSummary>;
 246
 247    fn summary(&self, _: <Self::Summary as sum_tree::Summary>::Context<'_>) -> Self::Summary {
 248        PathSummary {
 249            max_path: self.repo_path.as_ref().clone(),
 250            item_summary: self.status.summary(),
 251        }
 252    }
 253}
 254
 255impl sum_tree::KeyedItem for StatusEntry {
 256    type Key = PathKey;
 257
 258    fn key(&self) -> Self::Key {
 259        PathKey(self.repo_path.as_ref().clone())
 260    }
 261}
 262
 263#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
 264pub struct RepositoryId(pub u64);
 265
 266#[derive(Clone, Debug, Default, PartialEq, Eq)]
 267pub struct MergeDetails {
 268    pub merge_heads_by_conflicted_path: TreeMap<RepoPath, Vec<Option<SharedString>>>,
 269    pub message: Option<SharedString>,
 270}
 271
 272#[derive(Clone)]
 273pub enum CommitDataState {
 274    Loading,
 275    Loaded(Arc<GraphCommitData>),
 276}
 277
 278#[derive(Clone, Debug, PartialEq, Eq)]
 279pub struct RepositorySnapshot {
 280    pub id: RepositoryId,
 281    pub statuses_by_path: SumTree<StatusEntry>,
 282    pub work_directory_abs_path: Arc<Path>,
 283    /// The working directory of the original repository. For a normal
 284    /// checkout this equals `work_directory_abs_path`. For a git worktree
 285    /// checkout, this is the original repo's working directory — used to
 286    /// anchor new worktree creation so they don't nest.
 287    pub original_repo_abs_path: Arc<Path>,
 288    pub path_style: PathStyle,
 289    pub branch: Option<Branch>,
 290    pub head_commit: Option<CommitDetails>,
 291    pub scan_id: u64,
 292    pub merge: MergeDetails,
 293    pub remote_origin_url: Option<String>,
 294    pub remote_upstream_url: Option<String>,
 295    pub stash_entries: GitStash,
 296    pub linked_worktrees: Arc<[GitWorktree]>,
 297}
 298
 299type JobId = u64;
 300
 301#[derive(Clone, Debug, PartialEq, Eq)]
 302pub struct JobInfo {
 303    pub start: Instant,
 304    pub message: SharedString,
 305}
 306
 307struct GraphCommitDataHandler {
 308    _task: Task<()>,
 309    commit_data_request: smol::channel::Sender<Oid>,
 310}
 311
 312enum GraphCommitHandlerState {
 313    Starting,
 314    Open(GraphCommitDataHandler),
 315    Closed,
 316}
 317
 318pub struct InitialGitGraphData {
 319    fetch_task: Task<()>,
 320    pub error: Option<SharedString>,
 321    pub commit_data: Vec<Arc<InitialGraphCommitData>>,
 322    pub commit_oid_to_index: HashMap<Oid, usize>,
 323}
 324
 325pub struct GraphDataResponse<'a> {
 326    pub commits: &'a [Arc<InitialGraphCommitData>],
 327    pub is_loading: bool,
 328    pub error: Option<SharedString>,
 329}
 330
 331pub struct Repository {
 332    this: WeakEntity<Self>,
 333    snapshot: RepositorySnapshot,
 334    commit_message_buffer: Option<Entity<Buffer>>,
 335    git_store: WeakEntity<GitStore>,
 336    // For a local repository, holds paths that have had worktree events since the last status scan completed,
 337    // and that should be examined during the next status scan.
 338    paths_needing_status_update: Vec<Vec<RepoPath>>,
 339    job_sender: mpsc::UnboundedSender<GitJob>,
 340    active_jobs: HashMap<JobId, JobInfo>,
 341    pending_ops: SumTree<PendingOps>,
 342    job_id: JobId,
 343    askpass_delegates: Arc<Mutex<HashMap<u64, AskPassDelegate>>>,
 344    latest_askpass_id: u64,
 345    repository_state: Shared<Task<Result<RepositoryState, String>>>,
 346    initial_graph_data: HashMap<(LogSource, LogOrder), InitialGitGraphData>,
 347    graph_commit_data_handler: GraphCommitHandlerState,
 348    commit_data: HashMap<Oid, CommitDataState>,
 349}
 350
 351impl std::ops::Deref for Repository {
 352    type Target = RepositorySnapshot;
 353
 354    fn deref(&self) -> &Self::Target {
 355        &self.snapshot
 356    }
 357}
 358
 359#[derive(Clone)]
 360pub struct LocalRepositoryState {
 361    pub fs: Arc<dyn Fs>,
 362    pub backend: Arc<dyn GitRepository>,
 363    pub environment: Arc<HashMap<String, String>>,
 364}
 365
 366impl LocalRepositoryState {
 367    async fn new(
 368        work_directory_abs_path: Arc<Path>,
 369        dot_git_abs_path: Arc<Path>,
 370        project_environment: WeakEntity<ProjectEnvironment>,
 371        fs: Arc<dyn Fs>,
 372        is_trusted: bool,
 373        cx: &mut AsyncApp,
 374    ) -> anyhow::Result<Self> {
 375        let environment = project_environment
 376                .update(cx, |project_environment, cx| {
 377                    project_environment.local_directory_environment(&Shell::System, work_directory_abs_path.clone(), cx)
 378                })?
 379                .await
 380                .unwrap_or_else(|| {
 381                    log::error!("failed to get working directory environment for repository {work_directory_abs_path:?}");
 382                    HashMap::default()
 383                });
 384        let search_paths = environment.get("PATH").map(|val| val.to_owned());
 385        let backend = cx
 386            .background_spawn({
 387                let fs = fs.clone();
 388                async move {
 389                    let system_git_binary_path = search_paths
 390                        .and_then(|search_paths| {
 391                            which::which_in("git", Some(search_paths), &work_directory_abs_path)
 392                                .ok()
 393                        })
 394                        .or_else(|| which::which("git").ok());
 395                    fs.open_repo(&dot_git_abs_path, system_git_binary_path.as_deref())
 396                        .with_context(|| format!("opening repository at {dot_git_abs_path:?}"))
 397                }
 398            })
 399            .await?;
 400        backend.set_trusted(is_trusted);
 401        Ok(LocalRepositoryState {
 402            backend,
 403            environment: Arc::new(environment),
 404            fs,
 405        })
 406    }
 407}
 408
 409#[derive(Clone)]
 410pub struct RemoteRepositoryState {
 411    pub project_id: ProjectId,
 412    pub client: AnyProtoClient,
 413}
 414
 415#[derive(Clone)]
 416pub enum RepositoryState {
 417    Local(LocalRepositoryState),
 418    Remote(RemoteRepositoryState),
 419}
 420
 421#[derive(Clone, Debug, PartialEq, Eq)]
 422pub enum GitGraphEvent {
 423    CountUpdated(usize),
 424    FullyLoaded,
 425    LoadingError,
 426}
 427
 428#[derive(Clone, Debug, PartialEq, Eq)]
 429pub enum RepositoryEvent {
 430    StatusesChanged,
 431    BranchChanged,
 432    StashEntriesChanged,
 433    GitWorktreeListChanged,
 434    PendingOpsChanged { pending_ops: SumTree<PendingOps> },
 435    GraphEvent((LogSource, LogOrder), GitGraphEvent),
 436}
 437
 438#[derive(Clone, Debug)]
 439pub struct JobsUpdated;
 440
 441#[derive(Debug)]
 442pub enum GitStoreEvent {
 443    ActiveRepositoryChanged(Option<RepositoryId>),
 444    /// Bool is true when the repository that's updated is the active repository
 445    RepositoryUpdated(RepositoryId, RepositoryEvent, bool),
 446    RepositoryAdded,
 447    RepositoryRemoved(RepositoryId),
 448    IndexWriteError(anyhow::Error),
 449    JobsUpdated,
 450    ConflictsUpdated,
 451}
 452
 453impl EventEmitter<RepositoryEvent> for Repository {}
 454impl EventEmitter<JobsUpdated> for Repository {}
 455impl EventEmitter<GitStoreEvent> for GitStore {}
 456
 457pub struct GitJob {
 458    job: Box<dyn FnOnce(RepositoryState, &mut AsyncApp) -> Task<()>>,
 459    key: Option<GitJobKey>,
 460}
 461
 462#[derive(PartialEq, Eq)]
 463enum GitJobKey {
 464    WriteIndex(Vec<RepoPath>),
 465    ReloadBufferDiffBases,
 466    RefreshStatuses,
 467    ReloadGitState,
 468}
 469
 470impl GitStore {
 471    pub fn local(
 472        worktree_store: &Entity<WorktreeStore>,
 473        buffer_store: Entity<BufferStore>,
 474        environment: Entity<ProjectEnvironment>,
 475        fs: Arc<dyn Fs>,
 476        cx: &mut Context<Self>,
 477    ) -> Self {
 478        Self::new(
 479            worktree_store.clone(),
 480            buffer_store,
 481            GitStoreState::Local {
 482                next_repository_id: Arc::new(AtomicU64::new(1)),
 483                downstream: None,
 484                project_environment: environment,
 485                fs,
 486            },
 487            cx,
 488        )
 489    }
 490
 491    pub fn remote(
 492        worktree_store: &Entity<WorktreeStore>,
 493        buffer_store: Entity<BufferStore>,
 494        upstream_client: AnyProtoClient,
 495        project_id: u64,
 496        cx: &mut Context<Self>,
 497    ) -> Self {
 498        Self::new(
 499            worktree_store.clone(),
 500            buffer_store,
 501            GitStoreState::Remote {
 502                upstream_client,
 503                upstream_project_id: project_id,
 504                downstream: None,
 505            },
 506            cx,
 507        )
 508    }
 509
 510    fn new(
 511        worktree_store: Entity<WorktreeStore>,
 512        buffer_store: Entity<BufferStore>,
 513        state: GitStoreState,
 514        cx: &mut Context<Self>,
 515    ) -> Self {
 516        let mut _subscriptions = vec![
 517            cx.subscribe(&worktree_store, Self::on_worktree_store_event),
 518            cx.subscribe(&buffer_store, Self::on_buffer_store_event),
 519        ];
 520
 521        if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
 522            _subscriptions.push(cx.subscribe(&trusted_worktrees, Self::on_trusted_worktrees_event));
 523        }
 524
 525        GitStore {
 526            state,
 527            buffer_store,
 528            worktree_store,
 529            repositories: HashMap::default(),
 530            worktree_ids: HashMap::default(),
 531            active_repo_id: None,
 532            _subscriptions,
 533            loading_diffs: HashMap::default(),
 534            shared_diffs: HashMap::default(),
 535            diffs: HashMap::default(),
 536        }
 537    }
 538
 539    pub fn init(client: &AnyProtoClient) {
 540        client.add_entity_request_handler(Self::handle_get_remotes);
 541        client.add_entity_request_handler(Self::handle_get_branches);
 542        client.add_entity_request_handler(Self::handle_get_default_branch);
 543        client.add_entity_request_handler(Self::handle_change_branch);
 544        client.add_entity_request_handler(Self::handle_create_branch);
 545        client.add_entity_request_handler(Self::handle_rename_branch);
 546        client.add_entity_request_handler(Self::handle_create_remote);
 547        client.add_entity_request_handler(Self::handle_remove_remote);
 548        client.add_entity_request_handler(Self::handle_delete_branch);
 549        client.add_entity_request_handler(Self::handle_git_init);
 550        client.add_entity_request_handler(Self::handle_push);
 551        client.add_entity_request_handler(Self::handle_pull);
 552        client.add_entity_request_handler(Self::handle_fetch);
 553        client.add_entity_request_handler(Self::handle_stage);
 554        client.add_entity_request_handler(Self::handle_unstage);
 555        client.add_entity_request_handler(Self::handle_stash);
 556        client.add_entity_request_handler(Self::handle_stash_pop);
 557        client.add_entity_request_handler(Self::handle_stash_apply);
 558        client.add_entity_request_handler(Self::handle_stash_drop);
 559        client.add_entity_request_handler(Self::handle_commit);
 560        client.add_entity_request_handler(Self::handle_run_hook);
 561        client.add_entity_request_handler(Self::handle_reset);
 562        client.add_entity_request_handler(Self::handle_show);
 563        client.add_entity_request_handler(Self::handle_load_commit_diff);
 564        client.add_entity_request_handler(Self::handle_file_history);
 565        client.add_entity_request_handler(Self::handle_checkout_files);
 566        client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
 567        client.add_entity_request_handler(Self::handle_set_index_text);
 568        client.add_entity_request_handler(Self::handle_askpass);
 569        client.add_entity_request_handler(Self::handle_check_for_pushed_commits);
 570        client.add_entity_request_handler(Self::handle_git_diff);
 571        client.add_entity_request_handler(Self::handle_tree_diff);
 572        client.add_entity_request_handler(Self::handle_get_blob_content);
 573        client.add_entity_request_handler(Self::handle_open_unstaged_diff);
 574        client.add_entity_request_handler(Self::handle_open_uncommitted_diff);
 575        client.add_entity_message_handler(Self::handle_update_diff_bases);
 576        client.add_entity_request_handler(Self::handle_get_permalink_to_line);
 577        client.add_entity_request_handler(Self::handle_blame_buffer);
 578        client.add_entity_message_handler(Self::handle_update_repository);
 579        client.add_entity_message_handler(Self::handle_remove_repository);
 580        client.add_entity_request_handler(Self::handle_git_clone);
 581        client.add_entity_request_handler(Self::handle_get_worktrees);
 582        client.add_entity_request_handler(Self::handle_create_worktree);
 583        client.add_entity_request_handler(Self::handle_remove_worktree);
 584        client.add_entity_request_handler(Self::handle_rename_worktree);
 585    }
 586
 587    pub fn is_local(&self) -> bool {
 588        matches!(self.state, GitStoreState::Local { .. })
 589    }
 590    pub fn set_active_repo_for_path(&mut self, project_path: &ProjectPath, cx: &mut Context<Self>) {
 591        if let Some((repo, _)) = self.repository_and_path_for_project_path(project_path, cx) {
 592            let id = repo.read(cx).id;
 593            if self.active_repo_id != Some(id) {
 594                self.active_repo_id = Some(id);
 595                cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
 596            }
 597        }
 598    }
 599
 600    pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
 601        match &mut self.state {
 602            GitStoreState::Remote {
 603                downstream: downstream_client,
 604                ..
 605            } => {
 606                for repo in self.repositories.values() {
 607                    let update = repo.read(cx).snapshot.initial_update(project_id);
 608                    for update in split_repository_update(update) {
 609                        client.send(update).log_err();
 610                    }
 611                }
 612                *downstream_client = Some((client, ProjectId(project_id)));
 613            }
 614            GitStoreState::Local {
 615                downstream: downstream_client,
 616                ..
 617            } => {
 618                let mut snapshots = HashMap::default();
 619                let (updates_tx, mut updates_rx) = mpsc::unbounded();
 620                for repo in self.repositories.values() {
 621                    updates_tx
 622                        .unbounded_send(DownstreamUpdate::UpdateRepository(
 623                            repo.read(cx).snapshot.clone(),
 624                        ))
 625                        .ok();
 626                }
 627                *downstream_client = Some(LocalDownstreamState {
 628                    client: client.clone(),
 629                    project_id: ProjectId(project_id),
 630                    updates_tx,
 631                    _task: cx.spawn(async move |this, cx| {
 632                        cx.background_spawn(async move {
 633                            while let Some(update) = updates_rx.next().await {
 634                                match update {
 635                                    DownstreamUpdate::UpdateRepository(snapshot) => {
 636                                        if let Some(old_snapshot) = snapshots.get_mut(&snapshot.id)
 637                                        {
 638                                            let update =
 639                                                snapshot.build_update(old_snapshot, project_id);
 640                                            *old_snapshot = snapshot;
 641                                            for update in split_repository_update(update) {
 642                                                client.send(update)?;
 643                                            }
 644                                        } else {
 645                                            let update = snapshot.initial_update(project_id);
 646                                            for update in split_repository_update(update) {
 647                                                client.send(update)?;
 648                                            }
 649                                            snapshots.insert(snapshot.id, snapshot);
 650                                        }
 651                                    }
 652                                    DownstreamUpdate::RemoveRepository(id) => {
 653                                        client.send(proto::RemoveRepository {
 654                                            project_id,
 655                                            id: id.to_proto(),
 656                                        })?;
 657                                    }
 658                                }
 659                            }
 660                            anyhow::Ok(())
 661                        })
 662                        .await
 663                        .ok();
 664                        this.update(cx, |this, _| {
 665                            if let GitStoreState::Local {
 666                                downstream: downstream_client,
 667                                ..
 668                            } = &mut this.state
 669                            {
 670                                downstream_client.take();
 671                            } else {
 672                                unreachable!("unshared called on remote store");
 673                            }
 674                        })
 675                    }),
 676                });
 677            }
 678        }
 679    }
 680
 681    pub fn unshared(&mut self, _cx: &mut Context<Self>) {
 682        match &mut self.state {
 683            GitStoreState::Local {
 684                downstream: downstream_client,
 685                ..
 686            } => {
 687                downstream_client.take();
 688            }
 689            GitStoreState::Remote {
 690                downstream: downstream_client,
 691                ..
 692            } => {
 693                downstream_client.take();
 694            }
 695        }
 696        self.shared_diffs.clear();
 697    }
 698
 699    pub(crate) fn forget_shared_diffs_for(&mut self, peer_id: &proto::PeerId) {
 700        self.shared_diffs.remove(peer_id);
 701    }
 702
 703    pub fn active_repository(&self) -> Option<Entity<Repository>> {
 704        self.active_repo_id
 705            .as_ref()
 706            .map(|id| self.repositories[id].clone())
 707    }
 708
 709    pub fn open_unstaged_diff(
 710        &mut self,
 711        buffer: Entity<Buffer>,
 712        cx: &mut Context<Self>,
 713    ) -> Task<Result<Entity<BufferDiff>>> {
 714        let buffer_id = buffer.read(cx).remote_id();
 715        if let Some(diff_state) = self.diffs.get(&buffer_id)
 716            && let Some(unstaged_diff) = diff_state
 717                .read(cx)
 718                .unstaged_diff
 719                .as_ref()
 720                .and_then(|weak| weak.upgrade())
 721        {
 722            if let Some(task) =
 723                diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
 724            {
 725                return cx.background_executor().spawn(async move {
 726                    task.await;
 727                    Ok(unstaged_diff)
 728                });
 729            }
 730            return Task::ready(Ok(unstaged_diff));
 731        }
 732
 733        let Some((repo, repo_path)) =
 734            self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
 735        else {
 736            return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
 737        };
 738
 739        let task = self
 740            .loading_diffs
 741            .entry((buffer_id, DiffKind::Unstaged))
 742            .or_insert_with(|| {
 743                let staged_text = repo.update(cx, |repo, cx| {
 744                    repo.load_staged_text(buffer_id, repo_path, cx)
 745                });
 746                cx.spawn(async move |this, cx| {
 747                    Self::open_diff_internal(
 748                        this,
 749                        DiffKind::Unstaged,
 750                        staged_text.await.map(DiffBasesChange::SetIndex),
 751                        buffer,
 752                        cx,
 753                    )
 754                    .await
 755                    .map_err(Arc::new)
 756                })
 757                .shared()
 758            })
 759            .clone();
 760
 761        cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
 762    }
 763
 764    pub fn open_diff_since(
 765        &mut self,
 766        oid: Option<git::Oid>,
 767        buffer: Entity<Buffer>,
 768        repo: Entity<Repository>,
 769        cx: &mut Context<Self>,
 770    ) -> Task<Result<Entity<BufferDiff>>> {
 771        let buffer_id = buffer.read(cx).remote_id();
 772
 773        if let Some(diff_state) = self.diffs.get(&buffer_id)
 774            && let Some(oid_diff) = diff_state.read(cx).oid_diff(oid)
 775        {
 776            if let Some(task) =
 777                diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
 778            {
 779                return cx.background_executor().spawn(async move {
 780                    task.await;
 781                    Ok(oid_diff)
 782                });
 783            }
 784            return Task::ready(Ok(oid_diff));
 785        }
 786
 787        let diff_kind = DiffKind::SinceOid(oid);
 788        if let Some(task) = self.loading_diffs.get(&(buffer_id, diff_kind)) {
 789            let task = task.clone();
 790            return cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) });
 791        }
 792
 793        let task = cx
 794            .spawn(async move |this, cx| {
 795                let result: Result<Entity<BufferDiff>> = async {
 796                    let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
 797                    let language_registry =
 798                        buffer.update(cx, |buffer, _| buffer.language_registry());
 799                    let content: Option<Arc<str>> = match oid {
 800                        None => None,
 801                        Some(oid) => Some(
 802                            repo.update(cx, |repo, cx| repo.load_blob_content(oid, cx))
 803                                .await?
 804                                .into(),
 805                        ),
 806                    };
 807                    let buffer_diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, cx));
 808
 809                    buffer_diff
 810                        .update(cx, |buffer_diff, cx| {
 811                            buffer_diff.language_changed(
 812                                buffer_snapshot.language().cloned(),
 813                                language_registry,
 814                                cx,
 815                            );
 816                            buffer_diff.set_base_text(
 817                                content.clone(),
 818                                buffer_snapshot.language().cloned(),
 819                                buffer_snapshot.text,
 820                                cx,
 821                            )
 822                        })
 823                        .await?;
 824                    let unstaged_diff = this
 825                        .update(cx, |this, cx| this.open_unstaged_diff(buffer.clone(), cx))?
 826                        .await?;
 827                    buffer_diff.update(cx, |buffer_diff, _| {
 828                        buffer_diff.set_secondary_diff(unstaged_diff);
 829                    });
 830
 831                    this.update(cx, |this, cx| {
 832                        cx.subscribe(&buffer_diff, Self::on_buffer_diff_event)
 833                            .detach();
 834
 835                        this.loading_diffs.remove(&(buffer_id, diff_kind));
 836
 837                        let git_store = cx.weak_entity();
 838                        let diff_state = this
 839                            .diffs
 840                            .entry(buffer_id)
 841                            .or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
 842
 843                        diff_state.update(cx, |state, _| {
 844                            if let Some(oid) = oid {
 845                                if let Some(content) = content {
 846                                    state.oid_texts.insert(oid, content);
 847                                }
 848                            }
 849                            state.oid_diffs.insert(oid, buffer_diff.downgrade());
 850                        });
 851                    })?;
 852
 853                    Ok(buffer_diff)
 854                }
 855                .await;
 856                result.map_err(Arc::new)
 857            })
 858            .shared();
 859
 860        self.loading_diffs
 861            .insert((buffer_id, diff_kind), task.clone());
 862        cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
 863    }
 864
 865    #[ztracing::instrument(skip_all)]
 866    pub fn open_uncommitted_diff(
 867        &mut self,
 868        buffer: Entity<Buffer>,
 869        cx: &mut Context<Self>,
 870    ) -> Task<Result<Entity<BufferDiff>>> {
 871        let buffer_id = buffer.read(cx).remote_id();
 872
 873        if let Some(diff_state) = self.diffs.get(&buffer_id)
 874            && let Some(uncommitted_diff) = diff_state
 875                .read(cx)
 876                .uncommitted_diff
 877                .as_ref()
 878                .and_then(|weak| weak.upgrade())
 879        {
 880            if let Some(task) =
 881                diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
 882            {
 883                return cx.background_executor().spawn(async move {
 884                    task.await;
 885                    Ok(uncommitted_diff)
 886                });
 887            }
 888            return Task::ready(Ok(uncommitted_diff));
 889        }
 890
 891        let Some((repo, repo_path)) =
 892            self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
 893        else {
 894            return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
 895        };
 896
 897        let task = self
 898            .loading_diffs
 899            .entry((buffer_id, DiffKind::Uncommitted))
 900            .or_insert_with(|| {
 901                let changes = repo.update(cx, |repo, cx| {
 902                    repo.load_committed_text(buffer_id, repo_path, cx)
 903                });
 904
 905                // todo(lw): hot foreground spawn
 906                cx.spawn(async move |this, cx| {
 907                    Self::open_diff_internal(this, DiffKind::Uncommitted, changes.await, buffer, cx)
 908                        .await
 909                        .map_err(Arc::new)
 910                })
 911                .shared()
 912            })
 913            .clone();
 914
 915        cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
 916    }
 917
 918    #[ztracing::instrument(skip_all)]
 919    async fn open_diff_internal(
 920        this: WeakEntity<Self>,
 921        kind: DiffKind,
 922        texts: Result<DiffBasesChange>,
 923        buffer_entity: Entity<Buffer>,
 924        cx: &mut AsyncApp,
 925    ) -> Result<Entity<BufferDiff>> {
 926        let diff_bases_change = match texts {
 927            Err(e) => {
 928                this.update(cx, |this, cx| {
 929                    let buffer = buffer_entity.read(cx);
 930                    let buffer_id = buffer.remote_id();
 931                    this.loading_diffs.remove(&(buffer_id, kind));
 932                })?;
 933                return Err(e);
 934            }
 935            Ok(change) => change,
 936        };
 937
 938        this.update(cx, |this, cx| {
 939            let buffer = buffer_entity.read(cx);
 940            let buffer_id = buffer.remote_id();
 941            let language = buffer.language().cloned();
 942            let language_registry = buffer.language_registry();
 943            let text_snapshot = buffer.text_snapshot();
 944            this.loading_diffs.remove(&(buffer_id, kind));
 945
 946            let git_store = cx.weak_entity();
 947            let diff_state = this
 948                .diffs
 949                .entry(buffer_id)
 950                .or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
 951
 952            let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
 953
 954            cx.subscribe(&diff, Self::on_buffer_diff_event).detach();
 955            diff_state.update(cx, |diff_state, cx| {
 956                diff_state.language_changed = true;
 957                diff_state.language = language;
 958                diff_state.language_registry = language_registry;
 959
 960                match kind {
 961                    DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()),
 962                    DiffKind::Uncommitted => {
 963                        let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() {
 964                            diff
 965                        } else {
 966                            let unstaged_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
 967                            diff_state.unstaged_diff = Some(unstaged_diff.downgrade());
 968                            unstaged_diff
 969                        };
 970
 971                        diff.update(cx, |diff, _| diff.set_secondary_diff(unstaged_diff));
 972                        diff_state.uncommitted_diff = Some(diff.downgrade())
 973                    }
 974                    DiffKind::SinceOid(_) => {
 975                        unreachable!("open_diff_internal is not used for OID diffs")
 976                    }
 977                }
 978
 979                diff_state.diff_bases_changed(text_snapshot, Some(diff_bases_change), cx);
 980                let rx = diff_state.wait_for_recalculation();
 981
 982                anyhow::Ok(async move {
 983                    if let Some(rx) = rx {
 984                        rx.await;
 985                    }
 986                    Ok(diff)
 987                })
 988            })
 989        })??
 990        .await
 991    }
 992
 993    pub fn get_unstaged_diff(&self, buffer_id: BufferId, cx: &App) -> Option<Entity<BufferDiff>> {
 994        let diff_state = self.diffs.get(&buffer_id)?;
 995        diff_state.read(cx).unstaged_diff.as_ref()?.upgrade()
 996    }
 997
 998    pub fn get_uncommitted_diff(
 999        &self,
1000        buffer_id: BufferId,
1001        cx: &App,
1002    ) -> Option<Entity<BufferDiff>> {
1003        let diff_state = self.diffs.get(&buffer_id)?;
1004        diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade()
1005    }
1006
1007    pub fn get_diff_since_oid(
1008        &self,
1009        buffer_id: BufferId,
1010        oid: Option<git::Oid>,
1011        cx: &App,
1012    ) -> Option<Entity<BufferDiff>> {
1013        let diff_state = self.diffs.get(&buffer_id)?;
1014        diff_state.read(cx).oid_diff(oid)
1015    }
1016
1017    pub fn open_conflict_set(
1018        &mut self,
1019        buffer: Entity<Buffer>,
1020        cx: &mut Context<Self>,
1021    ) -> Entity<ConflictSet> {
1022        log::debug!("open conflict set");
1023        let buffer_id = buffer.read(cx).remote_id();
1024
1025        if let Some(git_state) = self.diffs.get(&buffer_id)
1026            && let Some(conflict_set) = git_state
1027                .read(cx)
1028                .conflict_set
1029                .as_ref()
1030                .and_then(|weak| weak.upgrade())
1031        {
1032            let conflict_set = conflict_set;
1033            let buffer_snapshot = buffer.read(cx).text_snapshot();
1034
1035            git_state.update(cx, |state, cx| {
1036                let _ = state.reparse_conflict_markers(buffer_snapshot, cx);
1037            });
1038
1039            return conflict_set;
1040        }
1041
1042        let is_unmerged = self
1043            .repository_and_path_for_buffer_id(buffer_id, cx)
1044            .is_some_and(|(repo, path)| repo.read(cx).snapshot.has_conflict(&path));
1045        let git_store = cx.weak_entity();
1046        let buffer_git_state = self
1047            .diffs
1048            .entry(buffer_id)
1049            .or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
1050        let conflict_set = cx.new(|cx| ConflictSet::new(buffer_id, is_unmerged, cx));
1051
1052        self._subscriptions
1053            .push(cx.subscribe(&conflict_set, |_, _, _, cx| {
1054                cx.emit(GitStoreEvent::ConflictsUpdated);
1055            }));
1056
1057        buffer_git_state.update(cx, |state, cx| {
1058            state.conflict_set = Some(conflict_set.downgrade());
1059            let buffer_snapshot = buffer.read(cx).text_snapshot();
1060            let _ = state.reparse_conflict_markers(buffer_snapshot, cx);
1061        });
1062
1063        conflict_set
1064    }
1065
1066    pub fn project_path_git_status(
1067        &self,
1068        project_path: &ProjectPath,
1069        cx: &App,
1070    ) -> Option<FileStatus> {
1071        let (repo, repo_path) = self.repository_and_path_for_project_path(project_path, cx)?;
1072        Some(repo.read(cx).status_for_path(&repo_path)?.status)
1073    }
1074
1075    pub fn checkpoint(&self, cx: &mut App) -> Task<Result<GitStoreCheckpoint>> {
1076        let mut work_directory_abs_paths = Vec::new();
1077        let mut checkpoints = Vec::new();
1078        for repository in self.repositories.values() {
1079            repository.update(cx, |repository, _| {
1080                work_directory_abs_paths.push(repository.snapshot.work_directory_abs_path.clone());
1081                checkpoints.push(repository.checkpoint().map(|checkpoint| checkpoint?));
1082            });
1083        }
1084
1085        cx.background_executor().spawn(async move {
1086            let checkpoints = future::try_join_all(checkpoints).await?;
1087            Ok(GitStoreCheckpoint {
1088                checkpoints_by_work_dir_abs_path: work_directory_abs_paths
1089                    .into_iter()
1090                    .zip(checkpoints)
1091                    .collect(),
1092            })
1093        })
1094    }
1095
1096    pub fn restore_checkpoint(
1097        &self,
1098        checkpoint: GitStoreCheckpoint,
1099        cx: &mut App,
1100    ) -> Task<Result<()>> {
1101        let repositories_by_work_dir_abs_path = self
1102            .repositories
1103            .values()
1104            .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
1105            .collect::<HashMap<_, _>>();
1106
1107        let mut tasks = Vec::new();
1108        for (work_dir_abs_path, checkpoint) in checkpoint.checkpoints_by_work_dir_abs_path {
1109            if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) {
1110                let restore = repository.update(cx, |repository, _| {
1111                    repository.restore_checkpoint(checkpoint)
1112                });
1113                tasks.push(async move { restore.await? });
1114            }
1115        }
1116        cx.background_spawn(async move {
1117            future::try_join_all(tasks).await?;
1118            Ok(())
1119        })
1120    }
1121
1122    /// Compares two checkpoints, returning true if they are equal.
1123    pub fn compare_checkpoints(
1124        &self,
1125        left: GitStoreCheckpoint,
1126        mut right: GitStoreCheckpoint,
1127        cx: &mut App,
1128    ) -> Task<Result<bool>> {
1129        let repositories_by_work_dir_abs_path = self
1130            .repositories
1131            .values()
1132            .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
1133            .collect::<HashMap<_, _>>();
1134
1135        let mut tasks = Vec::new();
1136        for (work_dir_abs_path, left_checkpoint) in left.checkpoints_by_work_dir_abs_path {
1137            if let Some(right_checkpoint) = right
1138                .checkpoints_by_work_dir_abs_path
1139                .remove(&work_dir_abs_path)
1140            {
1141                if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
1142                {
1143                    let compare = repository.update(cx, |repository, _| {
1144                        repository.compare_checkpoints(left_checkpoint, right_checkpoint)
1145                    });
1146
1147                    tasks.push(async move { compare.await? });
1148                }
1149            } else {
1150                return Task::ready(Ok(false));
1151            }
1152        }
1153        cx.background_spawn(async move {
1154            Ok(future::try_join_all(tasks)
1155                .await?
1156                .into_iter()
1157                .all(|result| result))
1158        })
1159    }
1160
1161    /// Blames a buffer.
1162    pub fn blame_buffer(
1163        &self,
1164        buffer: &Entity<Buffer>,
1165        version: Option<clock::Global>,
1166        cx: &mut Context<Self>,
1167    ) -> Task<Result<Option<Blame>>> {
1168        let buffer = buffer.read(cx);
1169        let Some((repo, repo_path)) =
1170            self.repository_and_path_for_buffer_id(buffer.remote_id(), cx)
1171        else {
1172            return Task::ready(Err(anyhow!("failed to find a git repository for buffer")));
1173        };
1174        let content = match &version {
1175            Some(version) => buffer.rope_for_version(version),
1176            None => buffer.as_rope().clone(),
1177        };
1178        let line_ending = buffer.line_ending();
1179        let version = version.unwrap_or(buffer.version());
1180        let buffer_id = buffer.remote_id();
1181
1182        let repo = repo.downgrade();
1183        cx.spawn(async move |_, cx| {
1184            let repository_state = repo
1185                .update(cx, |repo, _| repo.repository_state.clone())?
1186                .await
1187                .map_err(|err| anyhow::anyhow!(err))?;
1188            match repository_state {
1189                RepositoryState::Local(LocalRepositoryState { backend, .. }) => backend
1190                    .blame(repo_path.clone(), content, line_ending)
1191                    .await
1192                    .with_context(|| format!("Failed to blame {:?}", repo_path.as_ref()))
1193                    .map(Some),
1194                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
1195                    let response = client
1196                        .request(proto::BlameBuffer {
1197                            project_id: project_id.to_proto(),
1198                            buffer_id: buffer_id.into(),
1199                            version: serialize_version(&version),
1200                        })
1201                        .await?;
1202                    Ok(deserialize_blame_buffer_response(response))
1203                }
1204            }
1205        })
1206    }
1207
1208    pub fn file_history(
1209        &self,
1210        repo: &Entity<Repository>,
1211        path: RepoPath,
1212        cx: &mut App,
1213    ) -> Task<Result<git::repository::FileHistory>> {
1214        let rx = repo.update(cx, |repo, _| repo.file_history(path));
1215
1216        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
1217    }
1218
1219    pub fn file_history_paginated(
1220        &self,
1221        repo: &Entity<Repository>,
1222        path: RepoPath,
1223        skip: usize,
1224        limit: Option<usize>,
1225        cx: &mut App,
1226    ) -> Task<Result<git::repository::FileHistory>> {
1227        let rx = repo.update(cx, |repo, _| repo.file_history_paginated(path, skip, limit));
1228
1229        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
1230    }
1231
1232    pub fn get_permalink_to_line(
1233        &self,
1234        buffer: &Entity<Buffer>,
1235        selection: Range<u32>,
1236        cx: &mut App,
1237    ) -> Task<Result<url::Url>> {
1238        let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
1239            return Task::ready(Err(anyhow!("buffer has no file")));
1240        };
1241
1242        let Some((repo, repo_path)) = self.repository_and_path_for_project_path(
1243            &(file.worktree.read(cx).id(), file.path.clone()).into(),
1244            cx,
1245        ) else {
1246            // If we're not in a Git repo, check whether this is a Rust source
1247            // file in the Cargo registry (presumably opened with go-to-definition
1248            // from a normal Rust file). If so, we can put together a permalink
1249            // using crate metadata.
1250            if buffer
1251                .read(cx)
1252                .language()
1253                .is_none_or(|lang| lang.name() != "Rust")
1254            {
1255                return Task::ready(Err(anyhow!("no permalink available")));
1256            }
1257            let file_path = file.worktree.read(cx).absolutize(&file.path);
1258            return cx.spawn(async move |cx| {
1259                let provider_registry = cx.update(GitHostingProviderRegistry::default_global);
1260                get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
1261                    .context("no permalink available")
1262            });
1263        };
1264
1265        let buffer_id = buffer.read(cx).remote_id();
1266        let branch = repo.read(cx).branch.clone();
1267        let remote = branch
1268            .as_ref()
1269            .and_then(|b| b.upstream.as_ref())
1270            .and_then(|b| b.remote_name())
1271            .unwrap_or("origin")
1272            .to_string();
1273
1274        let rx = repo.update(cx, |repo, _| {
1275            repo.send_job(None, move |state, cx| async move {
1276                match state {
1277                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
1278                        let origin_url = backend
1279                            .remote_url(&remote)
1280                            .await
1281                            .with_context(|| format!("remote \"{remote}\" not found"))?;
1282
1283                        let sha = backend.head_sha().await.context("reading HEAD SHA")?;
1284
1285                        let provider_registry =
1286                            cx.update(GitHostingProviderRegistry::default_global);
1287
1288                        let (provider, remote) =
1289                            parse_git_remote_url(provider_registry, &origin_url)
1290                                .context("parsing Git remote URL")?;
1291
1292                        Ok(provider.build_permalink(
1293                            remote,
1294                            BuildPermalinkParams::new(&sha, &repo_path, Some(selection)),
1295                        ))
1296                    }
1297                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
1298                        let response = client
1299                            .request(proto::GetPermalinkToLine {
1300                                project_id: project_id.to_proto(),
1301                                buffer_id: buffer_id.into(),
1302                                selection: Some(proto::Range {
1303                                    start: selection.start as u64,
1304                                    end: selection.end as u64,
1305                                }),
1306                            })
1307                            .await?;
1308
1309                        url::Url::parse(&response.permalink).context("failed to parse permalink")
1310                    }
1311                }
1312            })
1313        });
1314        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
1315    }
1316
1317    fn downstream_client(&self) -> Option<(AnyProtoClient, ProjectId)> {
1318        match &self.state {
1319            GitStoreState::Local {
1320                downstream: downstream_client,
1321                ..
1322            } => downstream_client
1323                .as_ref()
1324                .map(|state| (state.client.clone(), state.project_id)),
1325            GitStoreState::Remote {
1326                downstream: downstream_client,
1327                ..
1328            } => downstream_client.clone(),
1329        }
1330    }
1331
1332    fn upstream_client(&self) -> Option<AnyProtoClient> {
1333        match &self.state {
1334            GitStoreState::Local { .. } => None,
1335            GitStoreState::Remote {
1336                upstream_client, ..
1337            } => Some(upstream_client.clone()),
1338        }
1339    }
1340
1341    fn on_worktree_store_event(
1342        &mut self,
1343        worktree_store: Entity<WorktreeStore>,
1344        event: &WorktreeStoreEvent,
1345        cx: &mut Context<Self>,
1346    ) {
1347        let GitStoreState::Local {
1348            project_environment,
1349            downstream,
1350            next_repository_id,
1351            fs,
1352        } = &self.state
1353        else {
1354            return;
1355        };
1356
1357        match event {
1358            WorktreeStoreEvent::WorktreeUpdatedEntries(worktree_id, updated_entries) => {
1359                if let Some(worktree) = self
1360                    .worktree_store
1361                    .read(cx)
1362                    .worktree_for_id(*worktree_id, cx)
1363                {
1364                    let paths_by_git_repo =
1365                        self.process_updated_entries(&worktree, updated_entries, cx);
1366                    let downstream = downstream
1367                        .as_ref()
1368                        .map(|downstream| downstream.updates_tx.clone());
1369                    cx.spawn(async move |_, cx| {
1370                        let paths_by_git_repo = paths_by_git_repo.await;
1371                        for (repo, paths) in paths_by_git_repo {
1372                            repo.update(cx, |repo, cx| {
1373                                repo.paths_changed(paths, downstream.clone(), cx);
1374                            });
1375                        }
1376                    })
1377                    .detach();
1378                }
1379            }
1380            WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => {
1381                let Some(worktree) = worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
1382                else {
1383                    return;
1384                };
1385                if !worktree.read(cx).is_visible() {
1386                    log::debug!(
1387                        "not adding repositories for local worktree {:?} because it's not visible",
1388                        worktree.read(cx).abs_path()
1389                    );
1390                    return;
1391                }
1392                self.update_repositories_from_worktree(
1393                    *worktree_id,
1394                    project_environment.clone(),
1395                    next_repository_id.clone(),
1396                    downstream
1397                        .as_ref()
1398                        .map(|downstream| downstream.updates_tx.clone()),
1399                    changed_repos.clone(),
1400                    fs.clone(),
1401                    cx,
1402                );
1403                self.local_worktree_git_repos_changed(worktree, changed_repos, cx);
1404            }
1405            WorktreeStoreEvent::WorktreeRemoved(_entity_id, worktree_id) => {
1406                let repos_without_worktree: Vec<RepositoryId> = self
1407                    .worktree_ids
1408                    .iter_mut()
1409                    .filter_map(|(repo_id, worktree_ids)| {
1410                        worktree_ids.remove(worktree_id);
1411                        if worktree_ids.is_empty() {
1412                            Some(*repo_id)
1413                        } else {
1414                            None
1415                        }
1416                    })
1417                    .collect();
1418                let is_active_repo_removed = repos_without_worktree
1419                    .iter()
1420                    .any(|repo_id| self.active_repo_id == Some(*repo_id));
1421
1422                for repo_id in repos_without_worktree {
1423                    self.repositories.remove(&repo_id);
1424                    self.worktree_ids.remove(&repo_id);
1425                    if let Some(updates_tx) =
1426                        downstream.as_ref().map(|downstream| &downstream.updates_tx)
1427                    {
1428                        updates_tx
1429                            .unbounded_send(DownstreamUpdate::RemoveRepository(repo_id))
1430                            .ok();
1431                    }
1432                }
1433
1434                if is_active_repo_removed {
1435                    if let Some((&repo_id, _)) = self.repositories.iter().next() {
1436                        self.active_repo_id = Some(repo_id);
1437                        cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(repo_id)));
1438                    } else {
1439                        self.active_repo_id = None;
1440                        cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
1441                    }
1442                }
1443            }
1444            _ => {}
1445        }
1446    }
1447    fn on_repository_event(
1448        &mut self,
1449        repo: Entity<Repository>,
1450        event: &RepositoryEvent,
1451        cx: &mut Context<Self>,
1452    ) {
1453        let id = repo.read(cx).id;
1454        let repo_snapshot = repo.read(cx).snapshot.clone();
1455        for (buffer_id, diff) in self.diffs.iter() {
1456            if let Some((buffer_repo, repo_path)) =
1457                self.repository_and_path_for_buffer_id(*buffer_id, cx)
1458                && buffer_repo == repo
1459            {
1460                diff.update(cx, |diff, cx| {
1461                    if let Some(conflict_set) = &diff.conflict_set {
1462                        let conflict_status_changed =
1463                            conflict_set.update(cx, |conflict_set, cx| {
1464                                let has_conflict = repo_snapshot.has_conflict(&repo_path);
1465                                conflict_set.set_has_conflict(has_conflict, cx)
1466                            })?;
1467                        if conflict_status_changed {
1468                            let buffer_store = self.buffer_store.read(cx);
1469                            if let Some(buffer) = buffer_store.get(*buffer_id) {
1470                                let _ = diff
1471                                    .reparse_conflict_markers(buffer.read(cx).text_snapshot(), cx);
1472                            }
1473                        }
1474                    }
1475                    anyhow::Ok(())
1476                })
1477                .ok();
1478            }
1479        }
1480        cx.emit(GitStoreEvent::RepositoryUpdated(
1481            id,
1482            event.clone(),
1483            self.active_repo_id == Some(id),
1484        ))
1485    }
1486
1487    fn on_jobs_updated(&mut self, _: Entity<Repository>, _: &JobsUpdated, cx: &mut Context<Self>) {
1488        cx.emit(GitStoreEvent::JobsUpdated)
1489    }
1490
1491    /// Update our list of repositories and schedule git scans in response to a notification from a worktree,
1492    fn update_repositories_from_worktree(
1493        &mut self,
1494        worktree_id: WorktreeId,
1495        project_environment: Entity<ProjectEnvironment>,
1496        next_repository_id: Arc<AtomicU64>,
1497        updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
1498        updated_git_repositories: UpdatedGitRepositoriesSet,
1499        fs: Arc<dyn Fs>,
1500        cx: &mut Context<Self>,
1501    ) {
1502        let mut removed_ids = Vec::new();
1503        for update in updated_git_repositories.iter() {
1504            if let Some((id, existing)) = self.repositories.iter().find(|(_, repo)| {
1505                let existing_work_directory_abs_path =
1506                    repo.read(cx).work_directory_abs_path.clone();
1507                Some(&existing_work_directory_abs_path)
1508                    == update.old_work_directory_abs_path.as_ref()
1509                    || Some(&existing_work_directory_abs_path)
1510                        == update.new_work_directory_abs_path.as_ref()
1511            }) {
1512                let repo_id = *id;
1513                if let Some(new_work_directory_abs_path) =
1514                    update.new_work_directory_abs_path.clone()
1515                {
1516                    self.worktree_ids
1517                        .entry(repo_id)
1518                        .or_insert_with(HashSet::new)
1519                        .insert(worktree_id);
1520                    existing.update(cx, |existing, cx| {
1521                        existing.snapshot.work_directory_abs_path = new_work_directory_abs_path;
1522                        existing.schedule_scan(updates_tx.clone(), cx);
1523                    });
1524                } else {
1525                    if let Some(worktree_ids) = self.worktree_ids.get_mut(&repo_id) {
1526                        worktree_ids.remove(&worktree_id);
1527                        if worktree_ids.is_empty() {
1528                            removed_ids.push(repo_id);
1529                        }
1530                    }
1531                }
1532            } else if let UpdatedGitRepository {
1533                new_work_directory_abs_path: Some(work_directory_abs_path),
1534                dot_git_abs_path: Some(dot_git_abs_path),
1535                repository_dir_abs_path: Some(repository_dir_abs_path),
1536                common_dir_abs_path: Some(common_dir_abs_path),
1537                ..
1538            } = update
1539            {
1540                let original_repo_abs_path: Arc<Path> = git::repository::original_repo_path(
1541                    work_directory_abs_path,
1542                    common_dir_abs_path,
1543                    repository_dir_abs_path,
1544                )
1545                .into();
1546                let id = RepositoryId(next_repository_id.fetch_add(1, atomic::Ordering::Release));
1547                let is_trusted = TrustedWorktrees::try_get_global(cx)
1548                    .map(|trusted_worktrees| {
1549                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1550                            trusted_worktrees.can_trust(&self.worktree_store, worktree_id, cx)
1551                        })
1552                    })
1553                    .unwrap_or(false);
1554                let git_store = cx.weak_entity();
1555                let repo = cx.new(|cx| {
1556                    let mut repo = Repository::local(
1557                        id,
1558                        work_directory_abs_path.clone(),
1559                        original_repo_abs_path.clone(),
1560                        dot_git_abs_path.clone(),
1561                        project_environment.downgrade(),
1562                        fs.clone(),
1563                        is_trusted,
1564                        git_store,
1565                        cx,
1566                    );
1567                    if let Some(updates_tx) = updates_tx.as_ref() {
1568                        // trigger an empty `UpdateRepository` to ensure remote active_repo_id is set correctly
1569                        updates_tx
1570                            .unbounded_send(DownstreamUpdate::UpdateRepository(repo.snapshot()))
1571                            .ok();
1572                    }
1573                    repo.schedule_scan(updates_tx.clone(), cx);
1574                    repo
1575                });
1576                self._subscriptions
1577                    .push(cx.subscribe(&repo, Self::on_repository_event));
1578                self._subscriptions
1579                    .push(cx.subscribe(&repo, Self::on_jobs_updated));
1580                self.repositories.insert(id, repo);
1581                self.worktree_ids.insert(id, HashSet::from([worktree_id]));
1582                cx.emit(GitStoreEvent::RepositoryAdded);
1583                self.active_repo_id.get_or_insert_with(|| {
1584                    cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
1585                    id
1586                });
1587            }
1588        }
1589
1590        for id in removed_ids {
1591            if self.active_repo_id == Some(id) {
1592                self.active_repo_id = None;
1593                cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
1594            }
1595            self.repositories.remove(&id);
1596            if let Some(updates_tx) = updates_tx.as_ref() {
1597                updates_tx
1598                    .unbounded_send(DownstreamUpdate::RemoveRepository(id))
1599                    .ok();
1600            }
1601        }
1602    }
1603
1604    fn on_trusted_worktrees_event(
1605        &mut self,
1606        _: Entity<TrustedWorktreesStore>,
1607        event: &TrustedWorktreesEvent,
1608        cx: &mut Context<Self>,
1609    ) {
1610        if !matches!(self.state, GitStoreState::Local { .. }) {
1611            return;
1612        }
1613
1614        let (is_trusted, event_paths) = match event {
1615            TrustedWorktreesEvent::Trusted(_, trusted_paths) => (true, trusted_paths),
1616            TrustedWorktreesEvent::Restricted(_, restricted_paths) => (false, restricted_paths),
1617        };
1618
1619        for (repo_id, worktree_ids) in &self.worktree_ids {
1620            if worktree_ids
1621                .iter()
1622                .any(|worktree_id| event_paths.contains(&PathTrust::Worktree(*worktree_id)))
1623            {
1624                if let Some(repo) = self.repositories.get(repo_id) {
1625                    let repository_state = repo.read(cx).repository_state.clone();
1626                    cx.background_spawn(async move {
1627                        if let Ok(RepositoryState::Local(state)) = repository_state.await {
1628                            state.backend.set_trusted(is_trusted);
1629                        }
1630                    })
1631                    .detach();
1632                }
1633            }
1634        }
1635    }
1636
1637    fn on_buffer_store_event(
1638        &mut self,
1639        _: Entity<BufferStore>,
1640        event: &BufferStoreEvent,
1641        cx: &mut Context<Self>,
1642    ) {
1643        match event {
1644            BufferStoreEvent::BufferAdded(buffer) => {
1645                cx.subscribe(buffer, |this, buffer, event, cx| {
1646                    if let BufferEvent::LanguageChanged(_) = event {
1647                        let buffer_id = buffer.read(cx).remote_id();
1648                        if let Some(diff_state) = this.diffs.get(&buffer_id) {
1649                            diff_state.update(cx, |diff_state, cx| {
1650                                diff_state.buffer_language_changed(buffer, cx);
1651                            });
1652                        }
1653                    }
1654                })
1655                .detach();
1656            }
1657            BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id) => {
1658                if let Some(diffs) = self.shared_diffs.get_mut(peer_id) {
1659                    diffs.remove(buffer_id);
1660                }
1661            }
1662            BufferStoreEvent::BufferDropped(buffer_id) => {
1663                self.diffs.remove(buffer_id);
1664                for diffs in self.shared_diffs.values_mut() {
1665                    diffs.remove(buffer_id);
1666                }
1667            }
1668            BufferStoreEvent::BufferChangedFilePath { buffer, .. } => {
1669                // Whenever a buffer's file path changes, it's possible that the
1670                // new path is actually a path that is being tracked by a git
1671                // repository. In that case, we'll want to update the buffer's
1672                // `BufferDiffState`, in case it already has one.
1673                let buffer_id = buffer.read(cx).remote_id();
1674                let diff_state = self.diffs.get(&buffer_id);
1675                let repo = self.repository_and_path_for_buffer_id(buffer_id, cx);
1676
1677                if let Some(diff_state) = diff_state
1678                    && let Some((repo, repo_path)) = repo
1679                {
1680                    let buffer = buffer.clone();
1681                    let diff_state = diff_state.clone();
1682
1683                    cx.spawn(async move |_git_store, cx| {
1684                        async {
1685                            let diff_bases_change = repo
1686                                .update(cx, |repo, cx| {
1687                                    repo.load_committed_text(buffer_id, repo_path, cx)
1688                                })
1689                                .await?;
1690
1691                            diff_state.update(cx, |diff_state, cx| {
1692                                let buffer_snapshot = buffer.read(cx).text_snapshot();
1693                                diff_state.diff_bases_changed(
1694                                    buffer_snapshot,
1695                                    Some(diff_bases_change),
1696                                    cx,
1697                                );
1698                            });
1699                            anyhow::Ok(())
1700                        }
1701                        .await
1702                        .log_err();
1703                    })
1704                    .detach();
1705                }
1706            }
1707        }
1708    }
1709
1710    pub fn recalculate_buffer_diffs(
1711        &mut self,
1712        buffers: Vec<Entity<Buffer>>,
1713        cx: &mut Context<Self>,
1714    ) -> impl Future<Output = ()> + use<> {
1715        let mut futures = Vec::new();
1716        for buffer in buffers {
1717            if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) {
1718                let buffer = buffer.read(cx).text_snapshot();
1719                diff_state.update(cx, |diff_state, cx| {
1720                    diff_state.recalculate_diffs(buffer.clone(), cx);
1721                    futures.extend(diff_state.wait_for_recalculation().map(FutureExt::boxed));
1722                });
1723                futures.push(diff_state.update(cx, |diff_state, cx| {
1724                    diff_state
1725                        .reparse_conflict_markers(buffer, cx)
1726                        .map(|_| {})
1727                        .boxed()
1728                }));
1729            }
1730        }
1731        async move {
1732            futures::future::join_all(futures).await;
1733        }
1734    }
1735
1736    fn on_buffer_diff_event(
1737        &mut self,
1738        diff: Entity<buffer_diff::BufferDiff>,
1739        event: &BufferDiffEvent,
1740        cx: &mut Context<Self>,
1741    ) {
1742        if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {
1743            let buffer_id = diff.read(cx).buffer_id;
1744            if let Some(diff_state) = self.diffs.get(&buffer_id) {
1745                let hunk_staging_operation_count = diff_state.update(cx, |diff_state, _| {
1746                    diff_state.hunk_staging_operation_count += 1;
1747                    diff_state.hunk_staging_operation_count
1748                });
1749                if let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) {
1750                    let recv = repo.update(cx, |repo, cx| {
1751                        log::debug!("hunks changed for {}", path.as_unix_str());
1752                        repo.spawn_set_index_text_job(
1753                            path,
1754                            new_index_text.as_ref().map(|rope| rope.to_string()),
1755                            Some(hunk_staging_operation_count),
1756                            cx,
1757                        )
1758                    });
1759                    let diff = diff.downgrade();
1760                    cx.spawn(async move |this, cx| {
1761                        if let Ok(Err(error)) = cx.background_spawn(recv).await {
1762                            diff.update(cx, |diff, cx| {
1763                                diff.clear_pending_hunks(cx);
1764                            })
1765                            .ok();
1766                            this.update(cx, |_, cx| cx.emit(GitStoreEvent::IndexWriteError(error)))
1767                                .ok();
1768                        }
1769                    })
1770                    .detach();
1771                }
1772            }
1773        }
1774    }
1775
1776    fn local_worktree_git_repos_changed(
1777        &mut self,
1778        worktree: Entity<Worktree>,
1779        changed_repos: &UpdatedGitRepositoriesSet,
1780        cx: &mut Context<Self>,
1781    ) {
1782        log::debug!("local worktree repos changed");
1783        debug_assert!(worktree.read(cx).is_local());
1784
1785        for repository in self.repositories.values() {
1786            repository.update(cx, |repository, cx| {
1787                let repo_abs_path = &repository.work_directory_abs_path;
1788                if changed_repos.iter().any(|update| {
1789                    update.old_work_directory_abs_path.as_ref() == Some(repo_abs_path)
1790                        || update.new_work_directory_abs_path.as_ref() == Some(repo_abs_path)
1791                }) {
1792                    repository.reload_buffer_diff_bases(cx);
1793                }
1794            });
1795        }
1796    }
1797
1798    pub fn repositories(&self) -> &HashMap<RepositoryId, Entity<Repository>> {
1799        &self.repositories
1800    }
1801
1802    pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
1803        let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?;
1804        let status = repo.read(cx).snapshot.status_for_path(&path)?;
1805        Some(status.status)
1806    }
1807
1808    pub fn repository_and_path_for_buffer_id(
1809        &self,
1810        buffer_id: BufferId,
1811        cx: &App,
1812    ) -> Option<(Entity<Repository>, RepoPath)> {
1813        let buffer = self.buffer_store.read(cx).get(buffer_id)?;
1814        let project_path = buffer.read(cx).project_path(cx)?;
1815        self.repository_and_path_for_project_path(&project_path, cx)
1816    }
1817
1818    pub fn repository_and_path_for_project_path(
1819        &self,
1820        path: &ProjectPath,
1821        cx: &App,
1822    ) -> Option<(Entity<Repository>, RepoPath)> {
1823        let abs_path = self.worktree_store.read(cx).absolutize(path, cx)?;
1824        self.repositories
1825            .values()
1826            .filter_map(|repo| {
1827                let repo_path = repo.read(cx).abs_path_to_repo_path(&abs_path)?;
1828                Some((repo.clone(), repo_path))
1829            })
1830            .max_by_key(|(repo, _)| repo.read(cx).work_directory_abs_path.clone())
1831    }
1832
1833    pub fn git_init(
1834        &self,
1835        path: Arc<Path>,
1836        fallback_branch_name: String,
1837        cx: &App,
1838    ) -> Task<Result<()>> {
1839        match &self.state {
1840            GitStoreState::Local { fs, .. } => {
1841                let fs = fs.clone();
1842                cx.background_executor()
1843                    .spawn(async move { fs.git_init(&path, fallback_branch_name).await })
1844            }
1845            GitStoreState::Remote {
1846                upstream_client,
1847                upstream_project_id: project_id,
1848                ..
1849            } => {
1850                let client = upstream_client.clone();
1851                let project_id = *project_id;
1852                cx.background_executor().spawn(async move {
1853                    client
1854                        .request(proto::GitInit {
1855                            project_id: project_id,
1856                            abs_path: path.to_string_lossy().into_owned(),
1857                            fallback_branch_name,
1858                        })
1859                        .await?;
1860                    Ok(())
1861                })
1862            }
1863        }
1864    }
1865
1866    pub fn git_clone(
1867        &self,
1868        repo: String,
1869        path: impl Into<Arc<std::path::Path>>,
1870        cx: &App,
1871    ) -> Task<Result<()>> {
1872        let path = path.into();
1873        match &self.state {
1874            GitStoreState::Local { fs, .. } => {
1875                let fs = fs.clone();
1876                cx.background_executor()
1877                    .spawn(async move { fs.git_clone(&repo, &path).await })
1878            }
1879            GitStoreState::Remote {
1880                upstream_client,
1881                upstream_project_id,
1882                ..
1883            } => {
1884                if upstream_client.is_via_collab() {
1885                    return Task::ready(Err(anyhow!(
1886                        "Git Clone isn't supported for project guests"
1887                    )));
1888                }
1889                let request = upstream_client.request(proto::GitClone {
1890                    project_id: *upstream_project_id,
1891                    abs_path: path.to_string_lossy().into_owned(),
1892                    remote_repo: repo,
1893                });
1894
1895                cx.background_spawn(async move {
1896                    let result = request.await?;
1897
1898                    match result.success {
1899                        true => Ok(()),
1900                        false => Err(anyhow!("Git Clone failed")),
1901                    }
1902                })
1903            }
1904        }
1905    }
1906
1907    async fn handle_update_repository(
1908        this: Entity<Self>,
1909        envelope: TypedEnvelope<proto::UpdateRepository>,
1910        mut cx: AsyncApp,
1911    ) -> Result<()> {
1912        this.update(&mut cx, |this, cx| {
1913            let path_style = this.worktree_store.read(cx).path_style();
1914            let mut update = envelope.payload;
1915
1916            let id = RepositoryId::from_proto(update.id);
1917            let client = this.upstream_client().context("no upstream client")?;
1918
1919            let original_repo_abs_path: Option<Arc<Path>> = update
1920                .original_repo_abs_path
1921                .as_deref()
1922                .map(|p| Path::new(p).into());
1923
1924            let mut repo_subscription = None;
1925            let repo = this.repositories.entry(id).or_insert_with(|| {
1926                let git_store = cx.weak_entity();
1927                let repo = cx.new(|cx| {
1928                    Repository::remote(
1929                        id,
1930                        Path::new(&update.abs_path).into(),
1931                        original_repo_abs_path.clone(),
1932                        path_style,
1933                        ProjectId(update.project_id),
1934                        client,
1935                        git_store,
1936                        cx,
1937                    )
1938                });
1939                repo_subscription = Some(cx.subscribe(&repo, Self::on_repository_event));
1940                cx.emit(GitStoreEvent::RepositoryAdded);
1941                repo
1942            });
1943            this._subscriptions.extend(repo_subscription);
1944
1945            repo.update(cx, {
1946                let update = update.clone();
1947                |repo, cx| repo.apply_remote_update(update, cx)
1948            })?;
1949
1950            this.active_repo_id.get_or_insert_with(|| {
1951                cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
1952                id
1953            });
1954
1955            if let Some((client, project_id)) = this.downstream_client() {
1956                update.project_id = project_id.to_proto();
1957                client.send(update).log_err();
1958            }
1959            Ok(())
1960        })
1961    }
1962
1963    async fn handle_remove_repository(
1964        this: Entity<Self>,
1965        envelope: TypedEnvelope<proto::RemoveRepository>,
1966        mut cx: AsyncApp,
1967    ) -> Result<()> {
1968        this.update(&mut cx, |this, cx| {
1969            let mut update = envelope.payload;
1970            let id = RepositoryId::from_proto(update.id);
1971            this.repositories.remove(&id);
1972            if let Some((client, project_id)) = this.downstream_client() {
1973                update.project_id = project_id.to_proto();
1974                client.send(update).log_err();
1975            }
1976            if this.active_repo_id == Some(id) {
1977                this.active_repo_id = None;
1978                cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
1979            }
1980            cx.emit(GitStoreEvent::RepositoryRemoved(id));
1981        });
1982        Ok(())
1983    }
1984
1985    async fn handle_git_init(
1986        this: Entity<Self>,
1987        envelope: TypedEnvelope<proto::GitInit>,
1988        cx: AsyncApp,
1989    ) -> Result<proto::Ack> {
1990        let path: Arc<Path> = PathBuf::from(envelope.payload.abs_path).into();
1991        let name = envelope.payload.fallback_branch_name;
1992        cx.update(|cx| this.read(cx).git_init(path, name, cx))
1993            .await?;
1994
1995        Ok(proto::Ack {})
1996    }
1997
1998    async fn handle_git_clone(
1999        this: Entity<Self>,
2000        envelope: TypedEnvelope<proto::GitClone>,
2001        cx: AsyncApp,
2002    ) -> Result<proto::GitCloneResponse> {
2003        let path: Arc<Path> = PathBuf::from(envelope.payload.abs_path).into();
2004        let repo_name = envelope.payload.remote_repo;
2005        let result = cx
2006            .update(|cx| this.read(cx).git_clone(repo_name, path, cx))
2007            .await;
2008
2009        Ok(proto::GitCloneResponse {
2010            success: result.is_ok(),
2011        })
2012    }
2013
2014    async fn handle_fetch(
2015        this: Entity<Self>,
2016        envelope: TypedEnvelope<proto::Fetch>,
2017        mut cx: AsyncApp,
2018    ) -> Result<proto::RemoteMessageResponse> {
2019        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2020        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2021        let fetch_options = FetchOptions::from_proto(envelope.payload.remote);
2022        let askpass_id = envelope.payload.askpass_id;
2023
2024        let askpass = make_remote_delegate(
2025            this,
2026            envelope.payload.project_id,
2027            repository_id,
2028            askpass_id,
2029            &mut cx,
2030        );
2031
2032        let remote_output = repository_handle
2033            .update(&mut cx, |repository_handle, cx| {
2034                repository_handle.fetch(fetch_options, askpass, cx)
2035            })
2036            .await??;
2037
2038        Ok(proto::RemoteMessageResponse {
2039            stdout: remote_output.stdout,
2040            stderr: remote_output.stderr,
2041        })
2042    }
2043
2044    async fn handle_push(
2045        this: Entity<Self>,
2046        envelope: TypedEnvelope<proto::Push>,
2047        mut cx: AsyncApp,
2048    ) -> Result<proto::RemoteMessageResponse> {
2049        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2050        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2051
2052        let askpass_id = envelope.payload.askpass_id;
2053        let askpass = make_remote_delegate(
2054            this,
2055            envelope.payload.project_id,
2056            repository_id,
2057            askpass_id,
2058            &mut cx,
2059        );
2060
2061        let options = envelope
2062            .payload
2063            .options
2064            .as_ref()
2065            .map(|_| match envelope.payload.options() {
2066                proto::push::PushOptions::SetUpstream => git::repository::PushOptions::SetUpstream,
2067                proto::push::PushOptions::Force => git::repository::PushOptions::Force,
2068            });
2069
2070        let branch_name = envelope.payload.branch_name.into();
2071        let remote_branch_name = envelope.payload.remote_branch_name.into();
2072        let remote_name = envelope.payload.remote_name.into();
2073
2074        let remote_output = repository_handle
2075            .update(&mut cx, |repository_handle, cx| {
2076                repository_handle.push(
2077                    branch_name,
2078                    remote_branch_name,
2079                    remote_name,
2080                    options,
2081                    askpass,
2082                    cx,
2083                )
2084            })
2085            .await??;
2086        Ok(proto::RemoteMessageResponse {
2087            stdout: remote_output.stdout,
2088            stderr: remote_output.stderr,
2089        })
2090    }
2091
2092    async fn handle_pull(
2093        this: Entity<Self>,
2094        envelope: TypedEnvelope<proto::Pull>,
2095        mut cx: AsyncApp,
2096    ) -> Result<proto::RemoteMessageResponse> {
2097        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2098        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2099        let askpass_id = envelope.payload.askpass_id;
2100        let askpass = make_remote_delegate(
2101            this,
2102            envelope.payload.project_id,
2103            repository_id,
2104            askpass_id,
2105            &mut cx,
2106        );
2107
2108        let branch_name = envelope.payload.branch_name.map(|name| name.into());
2109        let remote_name = envelope.payload.remote_name.into();
2110        let rebase = envelope.payload.rebase;
2111
2112        let remote_message = repository_handle
2113            .update(&mut cx, |repository_handle, cx| {
2114                repository_handle.pull(branch_name, remote_name, rebase, askpass, cx)
2115            })
2116            .await??;
2117
2118        Ok(proto::RemoteMessageResponse {
2119            stdout: remote_message.stdout,
2120            stderr: remote_message.stderr,
2121        })
2122    }
2123
2124    async fn handle_stage(
2125        this: Entity<Self>,
2126        envelope: TypedEnvelope<proto::Stage>,
2127        mut cx: AsyncApp,
2128    ) -> Result<proto::Ack> {
2129        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2130        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2131
2132        let entries = envelope
2133            .payload
2134            .paths
2135            .into_iter()
2136            .map(|path| RepoPath::new(&path))
2137            .collect::<Result<Vec<_>>>()?;
2138
2139        repository_handle
2140            .update(&mut cx, |repository_handle, cx| {
2141                repository_handle.stage_entries(entries, cx)
2142            })
2143            .await?;
2144        Ok(proto::Ack {})
2145    }
2146
2147    async fn handle_unstage(
2148        this: Entity<Self>,
2149        envelope: TypedEnvelope<proto::Unstage>,
2150        mut cx: AsyncApp,
2151    ) -> Result<proto::Ack> {
2152        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2153        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2154
2155        let entries = envelope
2156            .payload
2157            .paths
2158            .into_iter()
2159            .map(|path| RepoPath::new(&path))
2160            .collect::<Result<Vec<_>>>()?;
2161
2162        repository_handle
2163            .update(&mut cx, |repository_handle, cx| {
2164                repository_handle.unstage_entries(entries, cx)
2165            })
2166            .await?;
2167
2168        Ok(proto::Ack {})
2169    }
2170
2171    async fn handle_stash(
2172        this: Entity<Self>,
2173        envelope: TypedEnvelope<proto::Stash>,
2174        mut cx: AsyncApp,
2175    ) -> Result<proto::Ack> {
2176        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2177        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2178
2179        let entries = envelope
2180            .payload
2181            .paths
2182            .into_iter()
2183            .map(|path| RepoPath::new(&path))
2184            .collect::<Result<Vec<_>>>()?;
2185
2186        repository_handle
2187            .update(&mut cx, |repository_handle, cx| {
2188                repository_handle.stash_entries(entries, cx)
2189            })
2190            .await?;
2191
2192        Ok(proto::Ack {})
2193    }
2194
2195    async fn handle_stash_pop(
2196        this: Entity<Self>,
2197        envelope: TypedEnvelope<proto::StashPop>,
2198        mut cx: AsyncApp,
2199    ) -> Result<proto::Ack> {
2200        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2201        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2202        let stash_index = envelope.payload.stash_index.map(|i| i as usize);
2203
2204        repository_handle
2205            .update(&mut cx, |repository_handle, cx| {
2206                repository_handle.stash_pop(stash_index, cx)
2207            })
2208            .await?;
2209
2210        Ok(proto::Ack {})
2211    }
2212
2213    async fn handle_stash_apply(
2214        this: Entity<Self>,
2215        envelope: TypedEnvelope<proto::StashApply>,
2216        mut cx: AsyncApp,
2217    ) -> Result<proto::Ack> {
2218        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2219        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2220        let stash_index = envelope.payload.stash_index.map(|i| i as usize);
2221
2222        repository_handle
2223            .update(&mut cx, |repository_handle, cx| {
2224                repository_handle.stash_apply(stash_index, cx)
2225            })
2226            .await?;
2227
2228        Ok(proto::Ack {})
2229    }
2230
2231    async fn handle_stash_drop(
2232        this: Entity<Self>,
2233        envelope: TypedEnvelope<proto::StashDrop>,
2234        mut cx: AsyncApp,
2235    ) -> Result<proto::Ack> {
2236        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2237        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2238        let stash_index = envelope.payload.stash_index.map(|i| i as usize);
2239
2240        repository_handle
2241            .update(&mut cx, |repository_handle, cx| {
2242                repository_handle.stash_drop(stash_index, cx)
2243            })
2244            .await??;
2245
2246        Ok(proto::Ack {})
2247    }
2248
2249    async fn handle_set_index_text(
2250        this: Entity<Self>,
2251        envelope: TypedEnvelope<proto::SetIndexText>,
2252        mut cx: AsyncApp,
2253    ) -> Result<proto::Ack> {
2254        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2255        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2256        let repo_path = RepoPath::from_proto(&envelope.payload.path)?;
2257
2258        repository_handle
2259            .update(&mut cx, |repository_handle, cx| {
2260                repository_handle.spawn_set_index_text_job(
2261                    repo_path,
2262                    envelope.payload.text,
2263                    None,
2264                    cx,
2265                )
2266            })
2267            .await??;
2268        Ok(proto::Ack {})
2269    }
2270
2271    async fn handle_run_hook(
2272        this: Entity<Self>,
2273        envelope: TypedEnvelope<proto::RunGitHook>,
2274        mut cx: AsyncApp,
2275    ) -> Result<proto::Ack> {
2276        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2277        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2278        let hook = RunHook::from_proto(envelope.payload.hook).context("invalid hook")?;
2279        repository_handle
2280            .update(&mut cx, |repository_handle, cx| {
2281                repository_handle.run_hook(hook, cx)
2282            })
2283            .await??;
2284        Ok(proto::Ack {})
2285    }
2286
2287    async fn handle_commit(
2288        this: Entity<Self>,
2289        envelope: TypedEnvelope<proto::Commit>,
2290        mut cx: AsyncApp,
2291    ) -> Result<proto::Ack> {
2292        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2293        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2294        let askpass_id = envelope.payload.askpass_id;
2295
2296        let askpass = make_remote_delegate(
2297            this,
2298            envelope.payload.project_id,
2299            repository_id,
2300            askpass_id,
2301            &mut cx,
2302        );
2303
2304        let message = SharedString::from(envelope.payload.message);
2305        let name = envelope.payload.name.map(SharedString::from);
2306        let email = envelope.payload.email.map(SharedString::from);
2307        let options = envelope.payload.options.unwrap_or_default();
2308
2309        repository_handle
2310            .update(&mut cx, |repository_handle, cx| {
2311                repository_handle.commit(
2312                    message,
2313                    name.zip(email),
2314                    CommitOptions {
2315                        amend: options.amend,
2316                        signoff: options.signoff,
2317                        allow_empty: options.allow_empty,
2318                    },
2319                    askpass,
2320                    cx,
2321                )
2322            })
2323            .await??;
2324        Ok(proto::Ack {})
2325    }
2326
2327    async fn handle_get_remotes(
2328        this: Entity<Self>,
2329        envelope: TypedEnvelope<proto::GetRemotes>,
2330        mut cx: AsyncApp,
2331    ) -> Result<proto::GetRemotesResponse> {
2332        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2333        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2334
2335        let branch_name = envelope.payload.branch_name;
2336        let is_push = envelope.payload.is_push;
2337
2338        let remotes = repository_handle
2339            .update(&mut cx, |repository_handle, _| {
2340                repository_handle.get_remotes(branch_name, is_push)
2341            })
2342            .await??;
2343
2344        Ok(proto::GetRemotesResponse {
2345            remotes: remotes
2346                .into_iter()
2347                .map(|remotes| proto::get_remotes_response::Remote {
2348                    name: remotes.name.to_string(),
2349                })
2350                .collect::<Vec<_>>(),
2351        })
2352    }
2353
2354    async fn handle_get_worktrees(
2355        this: Entity<Self>,
2356        envelope: TypedEnvelope<proto::GitGetWorktrees>,
2357        mut cx: AsyncApp,
2358    ) -> Result<proto::GitWorktreesResponse> {
2359        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2360        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2361
2362        let worktrees = repository_handle
2363            .update(&mut cx, |repository_handle, _| {
2364                repository_handle.worktrees()
2365            })
2366            .await??;
2367
2368        Ok(proto::GitWorktreesResponse {
2369            worktrees: worktrees
2370                .into_iter()
2371                .map(|worktree| worktree_to_proto(&worktree))
2372                .collect::<Vec<_>>(),
2373        })
2374    }
2375
2376    async fn handle_create_worktree(
2377        this: Entity<Self>,
2378        envelope: TypedEnvelope<proto::GitCreateWorktree>,
2379        mut cx: AsyncApp,
2380    ) -> Result<proto::Ack> {
2381        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2382        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2383        let directory = PathBuf::from(envelope.payload.directory);
2384        let name = envelope.payload.name;
2385        let commit = envelope.payload.commit;
2386
2387        repository_handle
2388            .update(&mut cx, |repository_handle, _| {
2389                repository_handle.create_worktree(name, directory, commit)
2390            })
2391            .await??;
2392
2393        Ok(proto::Ack {})
2394    }
2395
2396    async fn handle_remove_worktree(
2397        this: Entity<Self>,
2398        envelope: TypedEnvelope<proto::GitRemoveWorktree>,
2399        mut cx: AsyncApp,
2400    ) -> Result<proto::Ack> {
2401        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2402        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2403        let path = PathBuf::from(envelope.payload.path);
2404        let force = envelope.payload.force;
2405
2406        repository_handle
2407            .update(&mut cx, |repository_handle, _| {
2408                repository_handle.remove_worktree(path, force)
2409            })
2410            .await??;
2411
2412        Ok(proto::Ack {})
2413    }
2414
2415    async fn handle_rename_worktree(
2416        this: Entity<Self>,
2417        envelope: TypedEnvelope<proto::GitRenameWorktree>,
2418        mut cx: AsyncApp,
2419    ) -> Result<proto::Ack> {
2420        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2421        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2422        let old_path = PathBuf::from(envelope.payload.old_path);
2423        let new_path = PathBuf::from(envelope.payload.new_path);
2424
2425        repository_handle
2426            .update(&mut cx, |repository_handle, _| {
2427                repository_handle.rename_worktree(old_path, new_path)
2428            })
2429            .await??;
2430
2431        Ok(proto::Ack {})
2432    }
2433
2434    async fn handle_get_branches(
2435        this: Entity<Self>,
2436        envelope: TypedEnvelope<proto::GitGetBranches>,
2437        mut cx: AsyncApp,
2438    ) -> Result<proto::GitBranchesResponse> {
2439        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2440        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2441
2442        let branches = repository_handle
2443            .update(&mut cx, |repository_handle, _| repository_handle.branches())
2444            .await??;
2445
2446        Ok(proto::GitBranchesResponse {
2447            branches: branches
2448                .into_iter()
2449                .map(|branch| branch_to_proto(&branch))
2450                .collect::<Vec<_>>(),
2451        })
2452    }
2453    async fn handle_get_default_branch(
2454        this: Entity<Self>,
2455        envelope: TypedEnvelope<proto::GetDefaultBranch>,
2456        mut cx: AsyncApp,
2457    ) -> Result<proto::GetDefaultBranchResponse> {
2458        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2459        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2460
2461        let branch = repository_handle
2462            .update(&mut cx, |repository_handle, _| {
2463                repository_handle.default_branch(false)
2464            })
2465            .await??
2466            .map(Into::into);
2467
2468        Ok(proto::GetDefaultBranchResponse { branch })
2469    }
2470    async fn handle_create_branch(
2471        this: Entity<Self>,
2472        envelope: TypedEnvelope<proto::GitCreateBranch>,
2473        mut cx: AsyncApp,
2474    ) -> Result<proto::Ack> {
2475        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2476        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2477        let branch_name = envelope.payload.branch_name;
2478
2479        repository_handle
2480            .update(&mut cx, |repository_handle, _| {
2481                repository_handle.create_branch(branch_name, None)
2482            })
2483            .await??;
2484
2485        Ok(proto::Ack {})
2486    }
2487
2488    async fn handle_change_branch(
2489        this: Entity<Self>,
2490        envelope: TypedEnvelope<proto::GitChangeBranch>,
2491        mut cx: AsyncApp,
2492    ) -> Result<proto::Ack> {
2493        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2494        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2495        let branch_name = envelope.payload.branch_name;
2496
2497        repository_handle
2498            .update(&mut cx, |repository_handle, _| {
2499                repository_handle.change_branch(branch_name)
2500            })
2501            .await??;
2502
2503        Ok(proto::Ack {})
2504    }
2505
2506    async fn handle_rename_branch(
2507        this: Entity<Self>,
2508        envelope: TypedEnvelope<proto::GitRenameBranch>,
2509        mut cx: AsyncApp,
2510    ) -> Result<proto::Ack> {
2511        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2512        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2513        let branch = envelope.payload.branch;
2514        let new_name = envelope.payload.new_name;
2515
2516        repository_handle
2517            .update(&mut cx, |repository_handle, _| {
2518                repository_handle.rename_branch(branch, new_name)
2519            })
2520            .await??;
2521
2522        Ok(proto::Ack {})
2523    }
2524
2525    async fn handle_create_remote(
2526        this: Entity<Self>,
2527        envelope: TypedEnvelope<proto::GitCreateRemote>,
2528        mut cx: AsyncApp,
2529    ) -> Result<proto::Ack> {
2530        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2531        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2532        let remote_name = envelope.payload.remote_name;
2533        let remote_url = envelope.payload.remote_url;
2534
2535        repository_handle
2536            .update(&mut cx, |repository_handle, _| {
2537                repository_handle.create_remote(remote_name, remote_url)
2538            })
2539            .await??;
2540
2541        Ok(proto::Ack {})
2542    }
2543
2544    async fn handle_delete_branch(
2545        this: Entity<Self>,
2546        envelope: TypedEnvelope<proto::GitDeleteBranch>,
2547        mut cx: AsyncApp,
2548    ) -> Result<proto::Ack> {
2549        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2550        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2551        let is_remote = envelope.payload.is_remote;
2552        let branch_name = envelope.payload.branch_name;
2553
2554        repository_handle
2555            .update(&mut cx, |repository_handle, _| {
2556                repository_handle.delete_branch(is_remote, branch_name)
2557            })
2558            .await??;
2559
2560        Ok(proto::Ack {})
2561    }
2562
2563    async fn handle_remove_remote(
2564        this: Entity<Self>,
2565        envelope: TypedEnvelope<proto::GitRemoveRemote>,
2566        mut cx: AsyncApp,
2567    ) -> Result<proto::Ack> {
2568        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2569        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2570        let remote_name = envelope.payload.remote_name;
2571
2572        repository_handle
2573            .update(&mut cx, |repository_handle, _| {
2574                repository_handle.remove_remote(remote_name)
2575            })
2576            .await??;
2577
2578        Ok(proto::Ack {})
2579    }
2580
2581    async fn handle_show(
2582        this: Entity<Self>,
2583        envelope: TypedEnvelope<proto::GitShow>,
2584        mut cx: AsyncApp,
2585    ) -> Result<proto::GitCommitDetails> {
2586        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2587        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2588
2589        let commit = repository_handle
2590            .update(&mut cx, |repository_handle, _| {
2591                repository_handle.show(envelope.payload.commit)
2592            })
2593            .await??;
2594        Ok(proto::GitCommitDetails {
2595            sha: commit.sha.into(),
2596            message: commit.message.into(),
2597            commit_timestamp: commit.commit_timestamp,
2598            author_email: commit.author_email.into(),
2599            author_name: commit.author_name.into(),
2600        })
2601    }
2602
2603    async fn handle_load_commit_diff(
2604        this: Entity<Self>,
2605        envelope: TypedEnvelope<proto::LoadCommitDiff>,
2606        mut cx: AsyncApp,
2607    ) -> Result<proto::LoadCommitDiffResponse> {
2608        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2609        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2610
2611        let commit_diff = repository_handle
2612            .update(&mut cx, |repository_handle, _| {
2613                repository_handle.load_commit_diff(envelope.payload.commit)
2614            })
2615            .await??;
2616        Ok(proto::LoadCommitDiffResponse {
2617            files: commit_diff
2618                .files
2619                .into_iter()
2620                .map(|file| proto::CommitFile {
2621                    path: file.path.to_proto(),
2622                    old_text: file.old_text,
2623                    new_text: file.new_text,
2624                    is_binary: file.is_binary,
2625                })
2626                .collect(),
2627        })
2628    }
2629
2630    async fn handle_file_history(
2631        this: Entity<Self>,
2632        envelope: TypedEnvelope<proto::GitFileHistory>,
2633        mut cx: AsyncApp,
2634    ) -> Result<proto::GitFileHistoryResponse> {
2635        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2636        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2637        let path = RepoPath::from_proto(&envelope.payload.path)?;
2638        let skip = envelope.payload.skip as usize;
2639        let limit = envelope.payload.limit.map(|l| l as usize);
2640
2641        let file_history = repository_handle
2642            .update(&mut cx, |repository_handle, _| {
2643                repository_handle.file_history_paginated(path, skip, limit)
2644            })
2645            .await??;
2646
2647        Ok(proto::GitFileHistoryResponse {
2648            entries: file_history
2649                .entries
2650                .into_iter()
2651                .map(|entry| proto::FileHistoryEntry {
2652                    sha: entry.sha.to_string(),
2653                    subject: entry.subject.to_string(),
2654                    message: entry.message.to_string(),
2655                    commit_timestamp: entry.commit_timestamp,
2656                    author_name: entry.author_name.to_string(),
2657                    author_email: entry.author_email.to_string(),
2658                })
2659                .collect(),
2660            path: file_history.path.to_proto(),
2661        })
2662    }
2663
2664    async fn handle_reset(
2665        this: Entity<Self>,
2666        envelope: TypedEnvelope<proto::GitReset>,
2667        mut cx: AsyncApp,
2668    ) -> Result<proto::Ack> {
2669        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2670        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2671
2672        let mode = match envelope.payload.mode() {
2673            git_reset::ResetMode::Soft => ResetMode::Soft,
2674            git_reset::ResetMode::Mixed => ResetMode::Mixed,
2675        };
2676
2677        repository_handle
2678            .update(&mut cx, |repository_handle, cx| {
2679                repository_handle.reset(envelope.payload.commit, mode, cx)
2680            })
2681            .await??;
2682        Ok(proto::Ack {})
2683    }
2684
2685    async fn handle_checkout_files(
2686        this: Entity<Self>,
2687        envelope: TypedEnvelope<proto::GitCheckoutFiles>,
2688        mut cx: AsyncApp,
2689    ) -> Result<proto::Ack> {
2690        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2691        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2692        let paths = envelope
2693            .payload
2694            .paths
2695            .iter()
2696            .map(|s| RepoPath::from_proto(s))
2697            .collect::<Result<Vec<_>>>()?;
2698
2699        repository_handle
2700            .update(&mut cx, |repository_handle, cx| {
2701                repository_handle.checkout_files(&envelope.payload.commit, paths, cx)
2702            })
2703            .await?;
2704        Ok(proto::Ack {})
2705    }
2706
2707    async fn handle_open_commit_message_buffer(
2708        this: Entity<Self>,
2709        envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
2710        mut cx: AsyncApp,
2711    ) -> Result<proto::OpenBufferResponse> {
2712        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2713        let repository = Self::repository_for_request(&this, repository_id, &mut cx)?;
2714        let buffer = repository
2715            .update(&mut cx, |repository, cx| {
2716                repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
2717            })
2718            .await?;
2719
2720        let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id());
2721        this.update(&mut cx, |this, cx| {
2722            this.buffer_store.update(cx, |buffer_store, cx| {
2723                buffer_store
2724                    .create_buffer_for_peer(
2725                        &buffer,
2726                        envelope.original_sender_id.unwrap_or(envelope.sender_id),
2727                        cx,
2728                    )
2729                    .detach_and_log_err(cx);
2730            })
2731        });
2732
2733        Ok(proto::OpenBufferResponse {
2734            buffer_id: buffer_id.to_proto(),
2735        })
2736    }
2737
2738    async fn handle_askpass(
2739        this: Entity<Self>,
2740        envelope: TypedEnvelope<proto::AskPassRequest>,
2741        mut cx: AsyncApp,
2742    ) -> Result<proto::AskPassResponse> {
2743        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2744        let repository = Self::repository_for_request(&this, repository_id, &mut cx)?;
2745
2746        let delegates = cx.update(|cx| repository.read(cx).askpass_delegates.clone());
2747        let Some(mut askpass) = delegates.lock().remove(&envelope.payload.askpass_id) else {
2748            debug_panic!("no askpass found");
2749            anyhow::bail!("no askpass found");
2750        };
2751
2752        let response = askpass
2753            .ask_password(envelope.payload.prompt)
2754            .await
2755            .ok_or_else(|| anyhow::anyhow!("askpass cancelled"))?;
2756
2757        delegates
2758            .lock()
2759            .insert(envelope.payload.askpass_id, askpass);
2760
2761        // In fact, we don't quite know what we're doing here, as we're sending askpass password unencrypted, but..
2762        Ok(proto::AskPassResponse {
2763            response: response.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)?,
2764        })
2765    }
2766
2767    async fn handle_check_for_pushed_commits(
2768        this: Entity<Self>,
2769        envelope: TypedEnvelope<proto::CheckForPushedCommits>,
2770        mut cx: AsyncApp,
2771    ) -> Result<proto::CheckForPushedCommitsResponse> {
2772        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2773        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2774
2775        let branches = repository_handle
2776            .update(&mut cx, |repository_handle, _| {
2777                repository_handle.check_for_pushed_commits()
2778            })
2779            .await??;
2780        Ok(proto::CheckForPushedCommitsResponse {
2781            pushed_to: branches
2782                .into_iter()
2783                .map(|commit| commit.to_string())
2784                .collect(),
2785        })
2786    }
2787
2788    async fn handle_git_diff(
2789        this: Entity<Self>,
2790        envelope: TypedEnvelope<proto::GitDiff>,
2791        mut cx: AsyncApp,
2792    ) -> Result<proto::GitDiffResponse> {
2793        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
2794        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
2795        let diff_type = match envelope.payload.diff_type() {
2796            proto::git_diff::DiffType::HeadToIndex => DiffType::HeadToIndex,
2797            proto::git_diff::DiffType::HeadToWorktree => DiffType::HeadToWorktree,
2798            proto::git_diff::DiffType::MergeBase => {
2799                let base_ref = envelope
2800                    .payload
2801                    .merge_base_ref
2802                    .ok_or_else(|| anyhow!("merge_base_ref is required for MergeBase diff type"))?;
2803                DiffType::MergeBase {
2804                    base_ref: base_ref.into(),
2805                }
2806            }
2807        };
2808
2809        let mut diff = repository_handle
2810            .update(&mut cx, |repository_handle, cx| {
2811                repository_handle.diff(diff_type, cx)
2812            })
2813            .await??;
2814        const ONE_MB: usize = 1_000_000;
2815        if diff.len() > ONE_MB {
2816            diff = diff.chars().take(ONE_MB).collect()
2817        }
2818
2819        Ok(proto::GitDiffResponse { diff })
2820    }
2821
2822    async fn handle_tree_diff(
2823        this: Entity<Self>,
2824        request: TypedEnvelope<proto::GetTreeDiff>,
2825        mut cx: AsyncApp,
2826    ) -> Result<proto::GetTreeDiffResponse> {
2827        let repository_id = RepositoryId(request.payload.repository_id);
2828        let diff_type = if request.payload.is_merge {
2829            DiffTreeType::MergeBase {
2830                base: request.payload.base.into(),
2831                head: request.payload.head.into(),
2832            }
2833        } else {
2834            DiffTreeType::Since {
2835                base: request.payload.base.into(),
2836                head: request.payload.head.into(),
2837            }
2838        };
2839
2840        let diff = this
2841            .update(&mut cx, |this, cx| {
2842                let repository = this.repositories().get(&repository_id)?;
2843                Some(repository.update(cx, |repo, cx| repo.diff_tree(diff_type, cx)))
2844            })
2845            .context("missing repository")?
2846            .await??;
2847
2848        Ok(proto::GetTreeDiffResponse {
2849            entries: diff
2850                .entries
2851                .into_iter()
2852                .map(|(path, status)| proto::TreeDiffStatus {
2853                    path: path.as_ref().to_proto(),
2854                    status: match status {
2855                        TreeDiffStatus::Added {} => proto::tree_diff_status::Status::Added.into(),
2856                        TreeDiffStatus::Modified { .. } => {
2857                            proto::tree_diff_status::Status::Modified.into()
2858                        }
2859                        TreeDiffStatus::Deleted { .. } => {
2860                            proto::tree_diff_status::Status::Deleted.into()
2861                        }
2862                    },
2863                    oid: match status {
2864                        TreeDiffStatus::Deleted { old } | TreeDiffStatus::Modified { old } => {
2865                            Some(old.to_string())
2866                        }
2867                        TreeDiffStatus::Added => None,
2868                    },
2869                })
2870                .collect(),
2871        })
2872    }
2873
2874    async fn handle_get_blob_content(
2875        this: Entity<Self>,
2876        request: TypedEnvelope<proto::GetBlobContent>,
2877        mut cx: AsyncApp,
2878    ) -> Result<proto::GetBlobContentResponse> {
2879        let oid = git::Oid::from_str(&request.payload.oid)?;
2880        let repository_id = RepositoryId(request.payload.repository_id);
2881        let content = this
2882            .update(&mut cx, |this, cx| {
2883                let repository = this.repositories().get(&repository_id)?;
2884                Some(repository.update(cx, |repo, cx| repo.load_blob_content(oid, cx)))
2885            })
2886            .context("missing repository")?
2887            .await?;
2888        Ok(proto::GetBlobContentResponse { content })
2889    }
2890
2891    async fn handle_open_unstaged_diff(
2892        this: Entity<Self>,
2893        request: TypedEnvelope<proto::OpenUnstagedDiff>,
2894        mut cx: AsyncApp,
2895    ) -> Result<proto::OpenUnstagedDiffResponse> {
2896        let buffer_id = BufferId::new(request.payload.buffer_id)?;
2897        let diff = this
2898            .update(&mut cx, |this, cx| {
2899                let buffer = this.buffer_store.read(cx).get(buffer_id)?;
2900                Some(this.open_unstaged_diff(buffer, cx))
2901            })
2902            .context("missing buffer")?
2903            .await?;
2904        this.update(&mut cx, |this, _| {
2905            let shared_diffs = this
2906                .shared_diffs
2907                .entry(request.original_sender_id.unwrap_or(request.sender_id))
2908                .or_default();
2909            shared_diffs.entry(buffer_id).or_default().unstaged = Some(diff.clone());
2910        });
2911        let staged_text = diff.read_with(&cx, |diff, cx| diff.base_text_string(cx));
2912        Ok(proto::OpenUnstagedDiffResponse { staged_text })
2913    }
2914
2915    async fn handle_open_uncommitted_diff(
2916        this: Entity<Self>,
2917        request: TypedEnvelope<proto::OpenUncommittedDiff>,
2918        mut cx: AsyncApp,
2919    ) -> Result<proto::OpenUncommittedDiffResponse> {
2920        let buffer_id = BufferId::new(request.payload.buffer_id)?;
2921        let diff = this
2922            .update(&mut cx, |this, cx| {
2923                let buffer = this.buffer_store.read(cx).get(buffer_id)?;
2924                Some(this.open_uncommitted_diff(buffer, cx))
2925            })
2926            .context("missing buffer")?
2927            .await?;
2928        this.update(&mut cx, |this, _| {
2929            let shared_diffs = this
2930                .shared_diffs
2931                .entry(request.original_sender_id.unwrap_or(request.sender_id))
2932                .or_default();
2933            shared_diffs.entry(buffer_id).or_default().uncommitted = Some(diff.clone());
2934        });
2935        Ok(diff.read_with(&cx, |diff, cx| {
2936            use proto::open_uncommitted_diff_response::Mode;
2937
2938            let unstaged_diff = diff.secondary_diff();
2939            let index_snapshot = unstaged_diff.and_then(|diff| {
2940                let diff = diff.read(cx);
2941                diff.base_text_exists().then(|| diff.base_text(cx))
2942            });
2943
2944            let mode;
2945            let staged_text;
2946            let committed_text;
2947            if diff.base_text_exists() {
2948                let committed_snapshot = diff.base_text(cx);
2949                committed_text = Some(committed_snapshot.text());
2950                if let Some(index_text) = index_snapshot {
2951                    if index_text.remote_id() == committed_snapshot.remote_id() {
2952                        mode = Mode::IndexMatchesHead;
2953                        staged_text = None;
2954                    } else {
2955                        mode = Mode::IndexAndHead;
2956                        staged_text = Some(index_text.text());
2957                    }
2958                } else {
2959                    mode = Mode::IndexAndHead;
2960                    staged_text = None;
2961                }
2962            } else {
2963                mode = Mode::IndexAndHead;
2964                committed_text = None;
2965                staged_text = index_snapshot.as_ref().map(|buffer| buffer.text());
2966            }
2967
2968            proto::OpenUncommittedDiffResponse {
2969                committed_text,
2970                staged_text,
2971                mode: mode.into(),
2972            }
2973        }))
2974    }
2975
2976    async fn handle_update_diff_bases(
2977        this: Entity<Self>,
2978        request: TypedEnvelope<proto::UpdateDiffBases>,
2979        mut cx: AsyncApp,
2980    ) -> Result<()> {
2981        let buffer_id = BufferId::new(request.payload.buffer_id)?;
2982        this.update(&mut cx, |this, cx| {
2983            if let Some(diff_state) = this.diffs.get_mut(&buffer_id)
2984                && let Some(buffer) = this.buffer_store.read(cx).get(buffer_id)
2985            {
2986                let buffer = buffer.read(cx).text_snapshot();
2987                diff_state.update(cx, |diff_state, cx| {
2988                    diff_state.handle_base_texts_updated(buffer, request.payload, cx);
2989                })
2990            }
2991        });
2992        Ok(())
2993    }
2994
2995    async fn handle_blame_buffer(
2996        this: Entity<Self>,
2997        envelope: TypedEnvelope<proto::BlameBuffer>,
2998        mut cx: AsyncApp,
2999    ) -> Result<proto::BlameBufferResponse> {
3000        let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
3001        let version = deserialize_version(&envelope.payload.version);
3002        let buffer = this.read_with(&cx, |this, cx| {
3003            this.buffer_store.read(cx).get_existing(buffer_id)
3004        })?;
3005        buffer
3006            .update(&mut cx, |buffer, _| {
3007                buffer.wait_for_version(version.clone())
3008            })
3009            .await?;
3010        let blame = this
3011            .update(&mut cx, |this, cx| {
3012                this.blame_buffer(&buffer, Some(version), cx)
3013            })
3014            .await?;
3015        Ok(serialize_blame_buffer_response(blame))
3016    }
3017
3018    async fn handle_get_permalink_to_line(
3019        this: Entity<Self>,
3020        envelope: TypedEnvelope<proto::GetPermalinkToLine>,
3021        mut cx: AsyncApp,
3022    ) -> Result<proto::GetPermalinkToLineResponse> {
3023        let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
3024        // let version = deserialize_version(&envelope.payload.version);
3025        let selection = {
3026            let proto_selection = envelope
3027                .payload
3028                .selection
3029                .context("no selection to get permalink for defined")?;
3030            proto_selection.start as u32..proto_selection.end as u32
3031        };
3032        let buffer = this.read_with(&cx, |this, cx| {
3033            this.buffer_store.read(cx).get_existing(buffer_id)
3034        })?;
3035        let permalink = this
3036            .update(&mut cx, |this, cx| {
3037                this.get_permalink_to_line(&buffer, selection, cx)
3038            })
3039            .await?;
3040        Ok(proto::GetPermalinkToLineResponse {
3041            permalink: permalink.to_string(),
3042        })
3043    }
3044
3045    fn repository_for_request(
3046        this: &Entity<Self>,
3047        id: RepositoryId,
3048        cx: &mut AsyncApp,
3049    ) -> Result<Entity<Repository>> {
3050        this.read_with(cx, |this, _| {
3051            this.repositories
3052                .get(&id)
3053                .context("missing repository handle")
3054                .cloned()
3055        })
3056    }
3057
3058    pub fn repo_snapshots(&self, cx: &App) -> HashMap<RepositoryId, RepositorySnapshot> {
3059        self.repositories
3060            .iter()
3061            .map(|(id, repo)| (*id, repo.read(cx).snapshot.clone()))
3062            .collect()
3063    }
3064
3065    fn process_updated_entries(
3066        &self,
3067        worktree: &Entity<Worktree>,
3068        updated_entries: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
3069        cx: &mut App,
3070    ) -> Task<HashMap<Entity<Repository>, Vec<RepoPath>>> {
3071        let path_style = worktree.read(cx).path_style();
3072        let mut repo_paths = self
3073            .repositories
3074            .values()
3075            .map(|repo| (repo.read(cx).work_directory_abs_path.clone(), repo.clone()))
3076            .collect::<Vec<_>>();
3077        let mut entries: Vec<_> = updated_entries
3078            .iter()
3079            .map(|(path, _, _)| path.clone())
3080            .collect();
3081        entries.sort();
3082        let worktree = worktree.read(cx);
3083
3084        let entries = entries
3085            .into_iter()
3086            .map(|path| worktree.absolutize(&path))
3087            .collect::<Arc<[_]>>();
3088
3089        let executor = cx.background_executor().clone();
3090        cx.background_executor().spawn(async move {
3091            repo_paths.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0));
3092            let mut paths_by_git_repo = HashMap::<_, Vec<_>>::default();
3093            let mut tasks = FuturesOrdered::new();
3094            for (repo_path, repo) in repo_paths.into_iter().rev() {
3095                let entries = entries.clone();
3096                let task = executor.spawn(async move {
3097                    // Find all repository paths that belong to this repo
3098                    let mut ix = entries.partition_point(|path| path < &*repo_path);
3099                    if ix == entries.len() {
3100                        return None;
3101                    };
3102
3103                    let mut paths = Vec::new();
3104                    // All paths prefixed by a given repo will constitute a continuous range.
3105                    while let Some(path) = entries.get(ix)
3106                        && let Some(repo_path) = RepositorySnapshot::abs_path_to_repo_path_inner(
3107                            &repo_path, path, path_style,
3108                        )
3109                    {
3110                        paths.push((repo_path, ix));
3111                        ix += 1;
3112                    }
3113                    if paths.is_empty() {
3114                        None
3115                    } else {
3116                        Some((repo, paths))
3117                    }
3118                });
3119                tasks.push_back(task);
3120            }
3121
3122            // Now, let's filter out the "duplicate" entries that were processed by multiple distinct repos.
3123            let mut path_was_used = vec![false; entries.len()];
3124            let tasks = tasks.collect::<Vec<_>>().await;
3125            // Process tasks from the back: iterating backwards allows us to see more-specific paths first.
3126            // We always want to assign a path to it's innermost repository.
3127            for t in tasks {
3128                let Some((repo, paths)) = t else {
3129                    continue;
3130                };
3131                let entry = paths_by_git_repo.entry(repo).or_default();
3132                for (repo_path, ix) in paths {
3133                    if path_was_used[ix] {
3134                        continue;
3135                    }
3136                    path_was_used[ix] = true;
3137                    entry.push(repo_path);
3138                }
3139            }
3140
3141            paths_by_git_repo
3142        })
3143    }
3144}
3145
3146impl BufferGitState {
3147    fn new(_git_store: WeakEntity<GitStore>) -> Self {
3148        Self {
3149            unstaged_diff: Default::default(),
3150            uncommitted_diff: Default::default(),
3151            oid_diffs: Default::default(),
3152            recalculate_diff_task: Default::default(),
3153            language: Default::default(),
3154            language_registry: Default::default(),
3155            recalculating_tx: postage::watch::channel_with(false).0,
3156            hunk_staging_operation_count: 0,
3157            hunk_staging_operation_count_as_of_write: 0,
3158            head_text: Default::default(),
3159            index_text: Default::default(),
3160            oid_texts: Default::default(),
3161            head_changed: Default::default(),
3162            index_changed: Default::default(),
3163            language_changed: Default::default(),
3164            conflict_updated_futures: Default::default(),
3165            conflict_set: Default::default(),
3166            reparse_conflict_markers_task: Default::default(),
3167        }
3168    }
3169
3170    #[ztracing::instrument(skip_all)]
3171    fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
3172        self.language = buffer.read(cx).language().cloned();
3173        self.language_changed = true;
3174        let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
3175    }
3176
3177    fn reparse_conflict_markers(
3178        &mut self,
3179        buffer: text::BufferSnapshot,
3180        cx: &mut Context<Self>,
3181    ) -> oneshot::Receiver<()> {
3182        let (tx, rx) = oneshot::channel();
3183
3184        let Some(conflict_set) = self
3185            .conflict_set
3186            .as_ref()
3187            .and_then(|conflict_set| conflict_set.upgrade())
3188        else {
3189            return rx;
3190        };
3191
3192        let old_snapshot = conflict_set.read_with(cx, |conflict_set, _| {
3193            if conflict_set.has_conflict {
3194                Some(conflict_set.snapshot())
3195            } else {
3196                None
3197            }
3198        });
3199
3200        if let Some(old_snapshot) = old_snapshot {
3201            self.conflict_updated_futures.push(tx);
3202            self.reparse_conflict_markers_task = Some(cx.spawn(async move |this, cx| {
3203                let (snapshot, changed_range) = cx
3204                    .background_spawn(async move {
3205                        let new_snapshot = ConflictSet::parse(&buffer);
3206                        let changed_range = old_snapshot.compare(&new_snapshot, &buffer);
3207                        (new_snapshot, changed_range)
3208                    })
3209                    .await;
3210                this.update(cx, |this, cx| {
3211                    if let Some(conflict_set) = &this.conflict_set {
3212                        conflict_set
3213                            .update(cx, |conflict_set, cx| {
3214                                conflict_set.set_snapshot(snapshot, changed_range, cx);
3215                            })
3216                            .ok();
3217                    }
3218                    let futures = std::mem::take(&mut this.conflict_updated_futures);
3219                    for tx in futures {
3220                        tx.send(()).ok();
3221                    }
3222                })
3223            }))
3224        }
3225
3226        rx
3227    }
3228
3229    fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
3230        self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
3231    }
3232
3233    fn uncommitted_diff(&self) -> Option<Entity<BufferDiff>> {
3234        self.uncommitted_diff.as_ref().and_then(|set| set.upgrade())
3235    }
3236
3237    fn oid_diff(&self, oid: Option<git::Oid>) -> Option<Entity<BufferDiff>> {
3238        self.oid_diffs.get(&oid).and_then(|weak| weak.upgrade())
3239    }
3240
3241    fn handle_base_texts_updated(
3242        &mut self,
3243        buffer: text::BufferSnapshot,
3244        message: proto::UpdateDiffBases,
3245        cx: &mut Context<Self>,
3246    ) {
3247        use proto::update_diff_bases::Mode;
3248
3249        let Some(mode) = Mode::from_i32(message.mode) else {
3250            return;
3251        };
3252
3253        let diff_bases_change = match mode {
3254            Mode::HeadOnly => DiffBasesChange::SetHead(message.committed_text),
3255            Mode::IndexOnly => DiffBasesChange::SetIndex(message.staged_text),
3256            Mode::IndexMatchesHead => DiffBasesChange::SetBoth(message.committed_text),
3257            Mode::IndexAndHead => DiffBasesChange::SetEach {
3258                index: message.staged_text,
3259                head: message.committed_text,
3260            },
3261        };
3262
3263        self.diff_bases_changed(buffer, Some(diff_bases_change), cx);
3264    }
3265
3266    pub fn wait_for_recalculation(&mut self) -> Option<impl Future<Output = ()> + use<>> {
3267        if *self.recalculating_tx.borrow() {
3268            let mut rx = self.recalculating_tx.subscribe();
3269            Some(async move {
3270                loop {
3271                    let is_recalculating = rx.recv().await;
3272                    if is_recalculating != Some(true) {
3273                        break;
3274                    }
3275                }
3276            })
3277        } else {
3278            None
3279        }
3280    }
3281
3282    fn diff_bases_changed(
3283        &mut self,
3284        buffer: text::BufferSnapshot,
3285        diff_bases_change: Option<DiffBasesChange>,
3286        cx: &mut Context<Self>,
3287    ) {
3288        match diff_bases_change {
3289            Some(DiffBasesChange::SetIndex(index)) => {
3290                self.index_text = index.map(|mut index| {
3291                    text::LineEnding::normalize(&mut index);
3292                    Arc::from(index.as_str())
3293                });
3294                self.index_changed = true;
3295            }
3296            Some(DiffBasesChange::SetHead(head)) => {
3297                self.head_text = head.map(|mut head| {
3298                    text::LineEnding::normalize(&mut head);
3299                    Arc::from(head.as_str())
3300                });
3301                self.head_changed = true;
3302            }
3303            Some(DiffBasesChange::SetBoth(text)) => {
3304                let text = text.map(|mut text| {
3305                    text::LineEnding::normalize(&mut text);
3306                    Arc::from(text.as_str())
3307                });
3308                self.head_text = text.clone();
3309                self.index_text = text;
3310                self.head_changed = true;
3311                self.index_changed = true;
3312            }
3313            Some(DiffBasesChange::SetEach { index, head }) => {
3314                self.index_text = index.map(|mut index| {
3315                    text::LineEnding::normalize(&mut index);
3316                    Arc::from(index.as_str())
3317                });
3318                self.index_changed = true;
3319                self.head_text = head.map(|mut head| {
3320                    text::LineEnding::normalize(&mut head);
3321                    Arc::from(head.as_str())
3322                });
3323                self.head_changed = true;
3324            }
3325            None => {}
3326        }
3327
3328        self.recalculate_diffs(buffer, cx)
3329    }
3330
3331    #[ztracing::instrument(skip_all)]
3332    fn recalculate_diffs(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
3333        *self.recalculating_tx.borrow_mut() = true;
3334
3335        let language = self.language.clone();
3336        let language_registry = self.language_registry.clone();
3337        let unstaged_diff = self.unstaged_diff();
3338        let uncommitted_diff = self.uncommitted_diff();
3339        let head = self.head_text.clone();
3340        let index = self.index_text.clone();
3341        let index_changed = self.index_changed;
3342        let head_changed = self.head_changed;
3343        let language_changed = self.language_changed;
3344        let prev_hunk_staging_operation_count = self.hunk_staging_operation_count_as_of_write;
3345        let index_matches_head = match (self.index_text.as_ref(), self.head_text.as_ref()) {
3346            (Some(index), Some(head)) => Arc::ptr_eq(index, head),
3347            (None, None) => true,
3348            _ => false,
3349        };
3350
3351        let oid_diffs: Vec<(Option<git::Oid>, Entity<BufferDiff>, Option<Arc<str>>)> = self
3352            .oid_diffs
3353            .iter()
3354            .filter_map(|(oid, weak)| {
3355                let base_text = oid.and_then(|oid| self.oid_texts.get(&oid).cloned());
3356                weak.upgrade().map(|diff| (*oid, diff, base_text))
3357            })
3358            .collect();
3359
3360        self.oid_diffs.retain(|oid, weak| {
3361            let alive = weak.upgrade().is_some();
3362            if !alive {
3363                if let Some(oid) = oid {
3364                    self.oid_texts.remove(oid);
3365                }
3366            }
3367            alive
3368        });
3369        self.recalculate_diff_task = Some(cx.spawn(async move |this, cx| {
3370            log::debug!(
3371                "start recalculating diffs for buffer {}",
3372                buffer.remote_id()
3373            );
3374
3375            let mut new_unstaged_diff = None;
3376            if let Some(unstaged_diff) = &unstaged_diff {
3377                new_unstaged_diff = Some(
3378                    cx.update(|cx| {
3379                        unstaged_diff.read(cx).update_diff(
3380                            buffer.clone(),
3381                            index,
3382                            index_changed.then_some(false),
3383                            language.clone(),
3384                            cx,
3385                        )
3386                    })
3387                    .await,
3388                );
3389            }
3390
3391            // Dropping BufferDiff can be expensive, so yield back to the event loop
3392            // for a bit
3393            yield_now().await;
3394
3395            let mut new_uncommitted_diff = None;
3396            if let Some(uncommitted_diff) = &uncommitted_diff {
3397                new_uncommitted_diff = if index_matches_head {
3398                    new_unstaged_diff.clone()
3399                } else {
3400                    Some(
3401                        cx.update(|cx| {
3402                            uncommitted_diff.read(cx).update_diff(
3403                                buffer.clone(),
3404                                head,
3405                                head_changed.then_some(true),
3406                                language.clone(),
3407                                cx,
3408                            )
3409                        })
3410                        .await,
3411                    )
3412                }
3413            }
3414
3415            // Dropping BufferDiff can be expensive, so yield back to the event loop
3416            // for a bit
3417            yield_now().await;
3418
3419            let cancel = this.update(cx, |this, _| {
3420                // This checks whether all pending stage/unstage operations
3421                // have quiesced (i.e. both the corresponding write and the
3422                // read of that write have completed). If not, then we cancel
3423                // this recalculation attempt to avoid invalidating pending
3424                // state too quickly; another recalculation will come along
3425                // later and clear the pending state once the state of the index has settled.
3426                if this.hunk_staging_operation_count > prev_hunk_staging_operation_count {
3427                    *this.recalculating_tx.borrow_mut() = false;
3428                    true
3429                } else {
3430                    false
3431                }
3432            })?;
3433            if cancel {
3434                log::debug!(
3435                    concat!(
3436                        "aborting recalculating diffs for buffer {}",
3437                        "due to subsequent hunk operations",
3438                    ),
3439                    buffer.remote_id()
3440                );
3441                return Ok(());
3442            }
3443
3444            let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
3445                unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
3446            {
3447                let task = unstaged_diff.update(cx, |diff, cx| {
3448                    // For git index buffer we skip assigning the language as we do not really need to perform any syntax highlighting on
3449                    // it. As a result, by skipping it we are potentially shaving off a lot of RSS plus we get a snappier feel for large diff
3450                    // view multibuffers.
3451                    diff.set_snapshot(new_unstaged_diff, &buffer, cx)
3452                });
3453                Some(task.await)
3454            } else {
3455                None
3456            };
3457
3458            yield_now().await;
3459
3460            if let Some((uncommitted_diff, new_uncommitted_diff)) =
3461                uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
3462            {
3463                uncommitted_diff
3464                    .update(cx, |diff, cx| {
3465                        if language_changed {
3466                            diff.language_changed(language.clone(), language_registry, cx);
3467                        }
3468                        diff.set_snapshot_with_secondary(
3469                            new_uncommitted_diff,
3470                            &buffer,
3471                            unstaged_changed_range.flatten(),
3472                            true,
3473                            cx,
3474                        )
3475                    })
3476                    .await;
3477            }
3478
3479            yield_now().await;
3480
3481            for (oid, oid_diff, base_text) in oid_diffs {
3482                let new_oid_diff = cx
3483                    .update(|cx| {
3484                        oid_diff.read(cx).update_diff(
3485                            buffer.clone(),
3486                            base_text,
3487                            None,
3488                            language.clone(),
3489                            cx,
3490                        )
3491                    })
3492                    .await;
3493
3494                oid_diff
3495                    .update(cx, |diff, cx| diff.set_snapshot(new_oid_diff, &buffer, cx))
3496                    .await;
3497
3498                log::debug!(
3499                    "finished recalculating oid diff for buffer {} oid {:?}",
3500                    buffer.remote_id(),
3501                    oid
3502                );
3503
3504                yield_now().await;
3505            }
3506
3507            log::debug!(
3508                "finished recalculating diffs for buffer {}",
3509                buffer.remote_id()
3510            );
3511
3512            if let Some(this) = this.upgrade() {
3513                this.update(cx, |this, _| {
3514                    this.index_changed = false;
3515                    this.head_changed = false;
3516                    this.language_changed = false;
3517                    *this.recalculating_tx.borrow_mut() = false;
3518                });
3519            }
3520
3521            Ok(())
3522        }));
3523    }
3524}
3525
3526fn make_remote_delegate(
3527    this: Entity<GitStore>,
3528    project_id: u64,
3529    repository_id: RepositoryId,
3530    askpass_id: u64,
3531    cx: &mut AsyncApp,
3532) -> AskPassDelegate {
3533    AskPassDelegate::new(cx, move |prompt, tx, cx| {
3534        this.update(cx, |this, cx| {
3535            let Some((client, _)) = this.downstream_client() else {
3536                return;
3537            };
3538            let response = client.request(proto::AskPassRequest {
3539                project_id,
3540                repository_id: repository_id.to_proto(),
3541                askpass_id,
3542                prompt,
3543            });
3544            cx.spawn(async move |_, _| {
3545                let mut response = response.await?.response;
3546                tx.send(EncryptedPassword::try_from(response.as_ref())?)
3547                    .ok();
3548                response.zeroize();
3549                anyhow::Ok(())
3550            })
3551            .detach_and_log_err(cx);
3552        });
3553    })
3554}
3555
3556impl RepositoryId {
3557    pub fn to_proto(self) -> u64 {
3558        self.0
3559    }
3560
3561    pub fn from_proto(id: u64) -> Self {
3562        RepositoryId(id)
3563    }
3564}
3565
3566impl RepositorySnapshot {
3567    fn empty(
3568        id: RepositoryId,
3569        work_directory_abs_path: Arc<Path>,
3570        original_repo_abs_path: Option<Arc<Path>>,
3571        path_style: PathStyle,
3572    ) -> Self {
3573        Self {
3574            id,
3575            statuses_by_path: Default::default(),
3576            original_repo_abs_path: original_repo_abs_path
3577                .unwrap_or_else(|| work_directory_abs_path.clone()),
3578            work_directory_abs_path,
3579            branch: None,
3580            head_commit: None,
3581            scan_id: 0,
3582            merge: Default::default(),
3583            remote_origin_url: None,
3584            remote_upstream_url: None,
3585            stash_entries: Default::default(),
3586            linked_worktrees: Arc::from([]),
3587            path_style,
3588        }
3589    }
3590
3591    fn initial_update(&self, project_id: u64) -> proto::UpdateRepository {
3592        proto::UpdateRepository {
3593            branch_summary: self.branch.as_ref().map(branch_to_proto),
3594            head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto),
3595            updated_statuses: self
3596                .statuses_by_path
3597                .iter()
3598                .map(|entry| entry.to_proto())
3599                .collect(),
3600            removed_statuses: Default::default(),
3601            current_merge_conflicts: self
3602                .merge
3603                .merge_heads_by_conflicted_path
3604                .iter()
3605                .map(|(repo_path, _)| repo_path.to_proto())
3606                .collect(),
3607            merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
3608            project_id,
3609            id: self.id.to_proto(),
3610            abs_path: self.work_directory_abs_path.to_string_lossy().into_owned(),
3611            entry_ids: vec![self.id.to_proto()],
3612            scan_id: self.scan_id,
3613            is_last_update: true,
3614            stash_entries: self
3615                .stash_entries
3616                .entries
3617                .iter()
3618                .map(stash_to_proto)
3619                .collect(),
3620            remote_upstream_url: self.remote_upstream_url.clone(),
3621            remote_origin_url: self.remote_origin_url.clone(),
3622            original_repo_abs_path: Some(
3623                self.original_repo_abs_path.to_string_lossy().into_owned(),
3624            ),
3625            linked_worktrees: self
3626                .linked_worktrees
3627                .iter()
3628                .map(worktree_to_proto)
3629                .collect(),
3630        }
3631    }
3632
3633    fn build_update(&self, old: &Self, project_id: u64) -> proto::UpdateRepository {
3634        let mut updated_statuses: Vec<proto::StatusEntry> = Vec::new();
3635        let mut removed_statuses: Vec<String> = Vec::new();
3636
3637        let mut new_statuses = self.statuses_by_path.iter().peekable();
3638        let mut old_statuses = old.statuses_by_path.iter().peekable();
3639
3640        let mut current_new_entry = new_statuses.next();
3641        let mut current_old_entry = old_statuses.next();
3642        loop {
3643            match (current_new_entry, current_old_entry) {
3644                (Some(new_entry), Some(old_entry)) => {
3645                    match new_entry.repo_path.cmp(&old_entry.repo_path) {
3646                        Ordering::Less => {
3647                            updated_statuses.push(new_entry.to_proto());
3648                            current_new_entry = new_statuses.next();
3649                        }
3650                        Ordering::Equal => {
3651                            if new_entry.status != old_entry.status
3652                                || new_entry.diff_stat != old_entry.diff_stat
3653                            {
3654                                updated_statuses.push(new_entry.to_proto());
3655                            }
3656                            current_old_entry = old_statuses.next();
3657                            current_new_entry = new_statuses.next();
3658                        }
3659                        Ordering::Greater => {
3660                            removed_statuses.push(old_entry.repo_path.to_proto());
3661                            current_old_entry = old_statuses.next();
3662                        }
3663                    }
3664                }
3665                (None, Some(old_entry)) => {
3666                    removed_statuses.push(old_entry.repo_path.to_proto());
3667                    current_old_entry = old_statuses.next();
3668                }
3669                (Some(new_entry), None) => {
3670                    updated_statuses.push(new_entry.to_proto());
3671                    current_new_entry = new_statuses.next();
3672                }
3673                (None, None) => break,
3674            }
3675        }
3676
3677        proto::UpdateRepository {
3678            branch_summary: self.branch.as_ref().map(branch_to_proto),
3679            head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto),
3680            updated_statuses,
3681            removed_statuses,
3682            current_merge_conflicts: self
3683                .merge
3684                .merge_heads_by_conflicted_path
3685                .iter()
3686                .map(|(path, _)| path.to_proto())
3687                .collect(),
3688            merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
3689            project_id,
3690            id: self.id.to_proto(),
3691            abs_path: self.work_directory_abs_path.to_string_lossy().into_owned(),
3692            entry_ids: vec![],
3693            scan_id: self.scan_id,
3694            is_last_update: true,
3695            stash_entries: self
3696                .stash_entries
3697                .entries
3698                .iter()
3699                .map(stash_to_proto)
3700                .collect(),
3701            remote_upstream_url: self.remote_upstream_url.clone(),
3702            remote_origin_url: self.remote_origin_url.clone(),
3703            original_repo_abs_path: Some(
3704                self.original_repo_abs_path.to_string_lossy().into_owned(),
3705            ),
3706            linked_worktrees: self
3707                .linked_worktrees
3708                .iter()
3709                .map(worktree_to_proto)
3710                .collect(),
3711        }
3712    }
3713
3714    /// The main worktree is the original checkout that other worktrees were
3715    /// created from.
3716    ///
3717    /// For example, if you had both `~/code/zed` and `~/code/worktrees/zed-2`,
3718    /// then `~/code/zed` is the main worktree and `~/code/worktrees/zed-2` is a linked worktree.
3719    ///
3720    /// Submodules also return `true` here, since they are not linked worktrees.
3721    pub fn is_main_worktree(&self) -> bool {
3722        self.work_directory_abs_path == self.original_repo_abs_path
3723    }
3724
3725    /// Returns true if this repository is a linked worktree, that is, one that
3726    /// was created from another worktree.
3727    ///
3728    /// Returns `false` for both the main worktree and submodules.
3729    pub fn is_linked_worktree(&self) -> bool {
3730        !self.is_main_worktree()
3731    }
3732
3733    pub fn linked_worktrees(&self) -> &[GitWorktree] {
3734        &self.linked_worktrees
3735    }
3736
3737    pub fn status(&self) -> impl Iterator<Item = StatusEntry> + '_ {
3738        self.statuses_by_path.iter().cloned()
3739    }
3740
3741    pub fn status_summary(&self) -> GitSummary {
3742        self.statuses_by_path.summary().item_summary
3743    }
3744
3745    pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
3746        self.statuses_by_path
3747            .get(&PathKey(path.as_ref().clone()), ())
3748            .cloned()
3749    }
3750
3751    pub fn diff_stat_for_path(&self, path: &RepoPath) -> Option<DiffStat> {
3752        self.statuses_by_path
3753            .get(&PathKey(path.as_ref().clone()), ())
3754            .and_then(|entry| entry.diff_stat)
3755    }
3756
3757    pub fn abs_path_to_repo_path(&self, abs_path: &Path) -> Option<RepoPath> {
3758        Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path, self.path_style)
3759    }
3760
3761    fn repo_path_to_abs_path(&self, repo_path: &RepoPath) -> PathBuf {
3762        self.path_style
3763            .join(&self.work_directory_abs_path, repo_path.as_std_path())
3764            .unwrap()
3765            .into()
3766    }
3767
3768    #[inline]
3769    fn abs_path_to_repo_path_inner(
3770        work_directory_abs_path: &Path,
3771        abs_path: &Path,
3772        path_style: PathStyle,
3773    ) -> Option<RepoPath> {
3774        let rel_path = path_style.strip_prefix(abs_path, work_directory_abs_path)?;
3775        Some(RepoPath::from_rel_path(&rel_path))
3776    }
3777
3778    pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool {
3779        self.merge
3780            .merge_heads_by_conflicted_path
3781            .contains_key(repo_path)
3782    }
3783
3784    pub fn has_conflict(&self, repo_path: &RepoPath) -> bool {
3785        let had_conflict_on_last_merge_head_change = self
3786            .merge
3787            .merge_heads_by_conflicted_path
3788            .contains_key(repo_path);
3789        let has_conflict_currently = self
3790            .status_for_path(repo_path)
3791            .is_some_and(|entry| entry.status.is_conflicted());
3792        had_conflict_on_last_merge_head_change || has_conflict_currently
3793    }
3794
3795    /// This is the name that will be displayed in the repository selector for this repository.
3796    pub fn display_name(&self) -> SharedString {
3797        self.work_directory_abs_path
3798            .file_name()
3799            .unwrap_or_default()
3800            .to_string_lossy()
3801            .to_string()
3802            .into()
3803    }
3804}
3805
3806pub fn stash_to_proto(entry: &StashEntry) -> proto::StashEntry {
3807    proto::StashEntry {
3808        oid: entry.oid.as_bytes().to_vec(),
3809        message: entry.message.clone(),
3810        branch: entry.branch.clone(),
3811        index: entry.index as u64,
3812        timestamp: entry.timestamp,
3813    }
3814}
3815
3816pub fn proto_to_stash(entry: &proto::StashEntry) -> Result<StashEntry> {
3817    Ok(StashEntry {
3818        oid: Oid::from_bytes(&entry.oid)?,
3819        message: entry.message.clone(),
3820        index: entry.index as usize,
3821        branch: entry.branch.clone(),
3822        timestamp: entry.timestamp,
3823    })
3824}
3825
3826impl MergeDetails {
3827    async fn update(
3828        &mut self,
3829        backend: &Arc<dyn GitRepository>,
3830        current_conflicted_paths: Vec<RepoPath>,
3831    ) -> Result<bool> {
3832        log::debug!("load merge details");
3833        self.message = backend.merge_message().await.map(SharedString::from);
3834        let heads = backend
3835            .revparse_batch(vec![
3836                "MERGE_HEAD".into(),
3837                "CHERRY_PICK_HEAD".into(),
3838                "REBASE_HEAD".into(),
3839                "REVERT_HEAD".into(),
3840                "APPLY_HEAD".into(),
3841            ])
3842            .await
3843            .log_err()
3844            .unwrap_or_default()
3845            .into_iter()
3846            .map(|opt| opt.map(SharedString::from))
3847            .collect::<Vec<_>>();
3848
3849        let mut conflicts_changed = false;
3850
3851        // Record the merge state for newly conflicted paths
3852        for path in &current_conflicted_paths {
3853            if self.merge_heads_by_conflicted_path.get(&path).is_none() {
3854                conflicts_changed = true;
3855                self.merge_heads_by_conflicted_path
3856                    .insert(path.clone(), heads.clone());
3857            }
3858        }
3859
3860        // Clear state for paths that are no longer conflicted and for which the merge heads have changed
3861        self.merge_heads_by_conflicted_path
3862            .retain(|path, old_merge_heads| {
3863                let keep = current_conflicted_paths.contains(path)
3864                    || (old_merge_heads == &heads
3865                        && old_merge_heads.iter().any(|head| head.is_some()));
3866                if !keep {
3867                    conflicts_changed = true;
3868                }
3869                keep
3870            });
3871
3872        Ok(conflicts_changed)
3873    }
3874}
3875
3876impl Repository {
3877    pub fn is_trusted(&self) -> bool {
3878        match self.repository_state.peek() {
3879            Some(Ok(RepositoryState::Local(state))) => state.backend.is_trusted(),
3880            _ => false,
3881        }
3882    }
3883
3884    pub fn snapshot(&self) -> RepositorySnapshot {
3885        self.snapshot.clone()
3886    }
3887
3888    pub fn pending_ops(&self) -> impl Iterator<Item = PendingOps> + '_ {
3889        self.pending_ops.iter().cloned()
3890    }
3891
3892    pub fn pending_ops_summary(&self) -> PathSummary<PendingOpsSummary> {
3893        self.pending_ops.summary().clone()
3894    }
3895
3896    pub fn pending_ops_for_path(&self, path: &RepoPath) -> Option<PendingOps> {
3897        self.pending_ops
3898            .get(&PathKey(path.as_ref().clone()), ())
3899            .cloned()
3900    }
3901
3902    fn local(
3903        id: RepositoryId,
3904        work_directory_abs_path: Arc<Path>,
3905        original_repo_abs_path: Arc<Path>,
3906        dot_git_abs_path: Arc<Path>,
3907        project_environment: WeakEntity<ProjectEnvironment>,
3908        fs: Arc<dyn Fs>,
3909        is_trusted: bool,
3910        git_store: WeakEntity<GitStore>,
3911        cx: &mut Context<Self>,
3912    ) -> Self {
3913        let snapshot = RepositorySnapshot::empty(
3914            id,
3915            work_directory_abs_path.clone(),
3916            Some(original_repo_abs_path),
3917            PathStyle::local(),
3918        );
3919        let state = cx
3920            .spawn(async move |_, cx| {
3921                LocalRepositoryState::new(
3922                    work_directory_abs_path,
3923                    dot_git_abs_path,
3924                    project_environment,
3925                    fs,
3926                    is_trusted,
3927                    cx,
3928                )
3929                .await
3930                .map_err(|err| err.to_string())
3931            })
3932            .shared();
3933        let job_sender = Repository::spawn_local_git_worker(state.clone(), cx);
3934        let state = cx
3935            .spawn(async move |_, _| {
3936                let state = state.await?;
3937                Ok(RepositoryState::Local(state))
3938            })
3939            .shared();
3940
3941        cx.subscribe_self(move |this, event: &RepositoryEvent, _| match event {
3942            RepositoryEvent::BranchChanged => {
3943                if this.scan_id > 1 {
3944                    this.initial_graph_data.clear();
3945                }
3946            }
3947            _ => {}
3948        })
3949        .detach();
3950
3951        Repository {
3952            this: cx.weak_entity(),
3953            git_store,
3954            snapshot,
3955            pending_ops: Default::default(),
3956            repository_state: state,
3957            commit_message_buffer: None,
3958            askpass_delegates: Default::default(),
3959            paths_needing_status_update: Default::default(),
3960            latest_askpass_id: 0,
3961            job_sender,
3962            job_id: 0,
3963            active_jobs: Default::default(),
3964            initial_graph_data: Default::default(),
3965            commit_data: Default::default(),
3966            graph_commit_data_handler: GraphCommitHandlerState::Closed,
3967        }
3968    }
3969
3970    fn remote(
3971        id: RepositoryId,
3972        work_directory_abs_path: Arc<Path>,
3973        original_repo_abs_path: Option<Arc<Path>>,
3974        path_style: PathStyle,
3975        project_id: ProjectId,
3976        client: AnyProtoClient,
3977        git_store: WeakEntity<GitStore>,
3978        cx: &mut Context<Self>,
3979    ) -> Self {
3980        let snapshot = RepositorySnapshot::empty(
3981            id,
3982            work_directory_abs_path,
3983            original_repo_abs_path,
3984            path_style,
3985        );
3986        let repository_state = RemoteRepositoryState { project_id, client };
3987        let job_sender = Self::spawn_remote_git_worker(repository_state.clone(), cx);
3988        let repository_state = Task::ready(Ok(RepositoryState::Remote(repository_state))).shared();
3989        Self {
3990            this: cx.weak_entity(),
3991            snapshot,
3992            commit_message_buffer: None,
3993            git_store,
3994            pending_ops: Default::default(),
3995            paths_needing_status_update: Default::default(),
3996            job_sender,
3997            repository_state,
3998            askpass_delegates: Default::default(),
3999            latest_askpass_id: 0,
4000            active_jobs: Default::default(),
4001            job_id: 0,
4002            initial_graph_data: Default::default(),
4003            commit_data: Default::default(),
4004            graph_commit_data_handler: GraphCommitHandlerState::Closed,
4005        }
4006    }
4007
4008    pub fn git_store(&self) -> Option<Entity<GitStore>> {
4009        self.git_store.upgrade()
4010    }
4011
4012    fn reload_buffer_diff_bases(&mut self, cx: &mut Context<Self>) {
4013        let this = cx.weak_entity();
4014        let git_store = self.git_store.clone();
4015        let _ = self.send_keyed_job(
4016            Some(GitJobKey::ReloadBufferDiffBases),
4017            None,
4018            |state, mut cx| async move {
4019                let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else {
4020                    log::error!("tried to recompute diffs for a non-local repository");
4021                    return Ok(());
4022                };
4023
4024                let Some(this) = this.upgrade() else {
4025                    return Ok(());
4026                };
4027
4028                let repo_diff_state_updates = this.update(&mut cx, |this, cx| {
4029                    git_store.update(cx, |git_store, cx| {
4030                        git_store
4031                            .diffs
4032                            .iter()
4033                            .filter_map(|(buffer_id, diff_state)| {
4034                                let buffer_store = git_store.buffer_store.read(cx);
4035                                let buffer = buffer_store.get(*buffer_id)?;
4036                                let file = File::from_dyn(buffer.read(cx).file())?;
4037                                let abs_path = file.worktree.read(cx).absolutize(&file.path);
4038                                let repo_path = this.abs_path_to_repo_path(&abs_path)?;
4039                                log::debug!(
4040                                    "start reload diff bases for repo path {}",
4041                                    repo_path.as_unix_str()
4042                                );
4043                                diff_state.update(cx, |diff_state, _| {
4044                                    let has_unstaged_diff = diff_state
4045                                        .unstaged_diff
4046                                        .as_ref()
4047                                        .is_some_and(|diff| diff.is_upgradable());
4048                                    let has_uncommitted_diff = diff_state
4049                                        .uncommitted_diff
4050                                        .as_ref()
4051                                        .is_some_and(|set| set.is_upgradable());
4052
4053                                    Some((
4054                                        buffer,
4055                                        repo_path,
4056                                        has_unstaged_diff.then(|| diff_state.index_text.clone()),
4057                                        has_uncommitted_diff.then(|| diff_state.head_text.clone()),
4058                                    ))
4059                                })
4060                            })
4061                            .collect::<Vec<_>>()
4062                    })
4063                })?;
4064
4065                let buffer_diff_base_changes = cx
4066                    .background_spawn(async move {
4067                        let mut changes = Vec::new();
4068                        for (buffer, repo_path, current_index_text, current_head_text) in
4069                            &repo_diff_state_updates
4070                        {
4071                            let index_text = if current_index_text.is_some() {
4072                                backend.load_index_text(repo_path.clone()).await
4073                            } else {
4074                                None
4075                            };
4076                            let head_text = if current_head_text.is_some() {
4077                                backend.load_committed_text(repo_path.clone()).await
4078                            } else {
4079                                None
4080                            };
4081
4082                            let change =
4083                                match (current_index_text.as_ref(), current_head_text.as_ref()) {
4084                                    (Some(current_index), Some(current_head)) => {
4085                                        let index_changed =
4086                                            index_text.as_deref() != current_index.as_deref();
4087                                        let head_changed =
4088                                            head_text.as_deref() != current_head.as_deref();
4089                                        if index_changed && head_changed {
4090                                            if index_text == head_text {
4091                                                Some(DiffBasesChange::SetBoth(head_text))
4092                                            } else {
4093                                                Some(DiffBasesChange::SetEach {
4094                                                    index: index_text,
4095                                                    head: head_text,
4096                                                })
4097                                            }
4098                                        } else if index_changed {
4099                                            Some(DiffBasesChange::SetIndex(index_text))
4100                                        } else if head_changed {
4101                                            Some(DiffBasesChange::SetHead(head_text))
4102                                        } else {
4103                                            None
4104                                        }
4105                                    }
4106                                    (Some(current_index), None) => {
4107                                        let index_changed =
4108                                            index_text.as_deref() != current_index.as_deref();
4109                                        index_changed
4110                                            .then_some(DiffBasesChange::SetIndex(index_text))
4111                                    }
4112                                    (None, Some(current_head)) => {
4113                                        let head_changed =
4114                                            head_text.as_deref() != current_head.as_deref();
4115                                        head_changed.then_some(DiffBasesChange::SetHead(head_text))
4116                                    }
4117                                    (None, None) => None,
4118                                };
4119
4120                            changes.push((buffer.clone(), change))
4121                        }
4122                        changes
4123                    })
4124                    .await;
4125
4126                git_store.update(&mut cx, |git_store, cx| {
4127                    for (buffer, diff_bases_change) in buffer_diff_base_changes {
4128                        let buffer_snapshot = buffer.read(cx).text_snapshot();
4129                        let buffer_id = buffer_snapshot.remote_id();
4130                        let Some(diff_state) = git_store.diffs.get(&buffer_id) else {
4131                            continue;
4132                        };
4133
4134                        let downstream_client = git_store.downstream_client();
4135                        diff_state.update(cx, |diff_state, cx| {
4136                            use proto::update_diff_bases::Mode;
4137
4138                            if let Some((diff_bases_change, (client, project_id))) =
4139                                diff_bases_change.clone().zip(downstream_client)
4140                            {
4141                                let (staged_text, committed_text, mode) = match diff_bases_change {
4142                                    DiffBasesChange::SetIndex(index) => {
4143                                        (index, None, Mode::IndexOnly)
4144                                    }
4145                                    DiffBasesChange::SetHead(head) => (None, head, Mode::HeadOnly),
4146                                    DiffBasesChange::SetEach { index, head } => {
4147                                        (index, head, Mode::IndexAndHead)
4148                                    }
4149                                    DiffBasesChange::SetBoth(text) => {
4150                                        (None, text, Mode::IndexMatchesHead)
4151                                    }
4152                                };
4153                                client
4154                                    .send(proto::UpdateDiffBases {
4155                                        project_id: project_id.to_proto(),
4156                                        buffer_id: buffer_id.to_proto(),
4157                                        staged_text,
4158                                        committed_text,
4159                                        mode: mode as i32,
4160                                    })
4161                                    .log_err();
4162                            }
4163
4164                            diff_state.diff_bases_changed(buffer_snapshot, diff_bases_change, cx);
4165                        });
4166                    }
4167                })
4168            },
4169        );
4170    }
4171
4172    pub fn send_job<F, Fut, R>(
4173        &mut self,
4174        status: Option<SharedString>,
4175        job: F,
4176    ) -> oneshot::Receiver<R>
4177    where
4178        F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static,
4179        Fut: Future<Output = R> + 'static,
4180        R: Send + 'static,
4181    {
4182        self.send_keyed_job(None, status, job)
4183    }
4184
4185    fn send_keyed_job<F, Fut, R>(
4186        &mut self,
4187        key: Option<GitJobKey>,
4188        status: Option<SharedString>,
4189        job: F,
4190    ) -> oneshot::Receiver<R>
4191    where
4192        F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static,
4193        Fut: Future<Output = R> + 'static,
4194        R: Send + 'static,
4195    {
4196        let (result_tx, result_rx) = futures::channel::oneshot::channel();
4197        let job_id = post_inc(&mut self.job_id);
4198        let this = self.this.clone();
4199        self.job_sender
4200            .unbounded_send(GitJob {
4201                key,
4202                job: Box::new(move |state, cx: &mut AsyncApp| {
4203                    let job = job(state, cx.clone());
4204                    cx.spawn(async move |cx| {
4205                        if let Some(s) = status.clone() {
4206                            this.update(cx, |this, cx| {
4207                                this.active_jobs.insert(
4208                                    job_id,
4209                                    JobInfo {
4210                                        start: Instant::now(),
4211                                        message: s.clone(),
4212                                    },
4213                                );
4214
4215                                cx.notify();
4216                            })
4217                            .ok();
4218                        }
4219                        let result = job.await;
4220
4221                        this.update(cx, |this, cx| {
4222                            this.active_jobs.remove(&job_id);
4223                            cx.notify();
4224                        })
4225                        .ok();
4226
4227                        result_tx.send(result).ok();
4228                    })
4229                }),
4230            })
4231            .ok();
4232        result_rx
4233    }
4234
4235    pub fn set_as_active_repository(&self, cx: &mut Context<Self>) {
4236        let Some(git_store) = self.git_store.upgrade() else {
4237            return;
4238        };
4239        let entity = cx.entity();
4240        git_store.update(cx, |git_store, cx| {
4241            let Some((&id, _)) = git_store
4242                .repositories
4243                .iter()
4244                .find(|(_, handle)| *handle == &entity)
4245            else {
4246                return;
4247            };
4248            git_store.active_repo_id = Some(id);
4249            cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
4250        });
4251    }
4252
4253    pub fn cached_status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
4254        self.snapshot.status()
4255    }
4256
4257    pub fn diff_stat_for_path(&self, path: &RepoPath) -> Option<DiffStat> {
4258        self.snapshot.diff_stat_for_path(path)
4259    }
4260
4261    pub fn cached_stash(&self) -> GitStash {
4262        self.snapshot.stash_entries.clone()
4263    }
4264
4265    pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option<ProjectPath> {
4266        let git_store = self.git_store.upgrade()?;
4267        let worktree_store = git_store.read(cx).worktree_store.read(cx);
4268        let abs_path = self.snapshot.repo_path_to_abs_path(path);
4269        let abs_path = SanitizedPath::new(&abs_path);
4270        let (worktree, relative_path) = worktree_store.find_worktree(abs_path, cx)?;
4271        Some(ProjectPath {
4272            worktree_id: worktree.read(cx).id(),
4273            path: relative_path,
4274        })
4275    }
4276
4277    pub fn project_path_to_repo_path(&self, path: &ProjectPath, cx: &App) -> Option<RepoPath> {
4278        let git_store = self.git_store.upgrade()?;
4279        let worktree_store = git_store.read(cx).worktree_store.read(cx);
4280        let abs_path = worktree_store.absolutize(path, cx)?;
4281        self.snapshot.abs_path_to_repo_path(&abs_path)
4282    }
4283
4284    pub fn contains_sub_repo(&self, other: &Entity<Self>, cx: &App) -> bool {
4285        other
4286            .read(cx)
4287            .snapshot
4288            .work_directory_abs_path
4289            .starts_with(&self.snapshot.work_directory_abs_path)
4290    }
4291
4292    pub fn open_commit_buffer(
4293        &mut self,
4294        languages: Option<Arc<LanguageRegistry>>,
4295        buffer_store: Entity<BufferStore>,
4296        cx: &mut Context<Self>,
4297    ) -> Task<Result<Entity<Buffer>>> {
4298        let id = self.id;
4299        if let Some(buffer) = self.commit_message_buffer.clone() {
4300            return Task::ready(Ok(buffer));
4301        }
4302        let this = cx.weak_entity();
4303
4304        let rx = self.send_job(None, move |state, mut cx| async move {
4305            let Some(this) = this.upgrade() else {
4306                bail!("git store was dropped");
4307            };
4308            match state {
4309                RepositoryState::Local(..) => {
4310                    this.update(&mut cx, |_, cx| {
4311                        Self::open_local_commit_buffer(languages, buffer_store, cx)
4312                    })
4313                    .await
4314                }
4315                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
4316                    let request = client.request(proto::OpenCommitMessageBuffer {
4317                        project_id: project_id.0,
4318                        repository_id: id.to_proto(),
4319                    });
4320                    let response = request.await.context("requesting to open commit buffer")?;
4321                    let buffer_id = BufferId::new(response.buffer_id)?;
4322                    let buffer = buffer_store
4323                        .update(&mut cx, |buffer_store, cx| {
4324                            buffer_store.wait_for_remote_buffer(buffer_id, cx)
4325                        })
4326                        .await?;
4327                    if let Some(language_registry) = languages {
4328                        let git_commit_language =
4329                            language_registry.language_for_name("Git Commit").await?;
4330                        buffer.update(&mut cx, |buffer, cx| {
4331                            buffer.set_language(Some(git_commit_language), cx);
4332                        });
4333                    }
4334                    this.update(&mut cx, |this, _| {
4335                        this.commit_message_buffer = Some(buffer.clone());
4336                    });
4337                    Ok(buffer)
4338                }
4339            }
4340        });
4341
4342        cx.spawn(|_, _: &mut AsyncApp| async move { rx.await? })
4343    }
4344
4345    fn open_local_commit_buffer(
4346        language_registry: Option<Arc<LanguageRegistry>>,
4347        buffer_store: Entity<BufferStore>,
4348        cx: &mut Context<Self>,
4349    ) -> Task<Result<Entity<Buffer>>> {
4350        cx.spawn(async move |repository, cx| {
4351            let git_commit_language = match language_registry {
4352                Some(language_registry) => {
4353                    Some(language_registry.language_for_name("Git Commit").await?)
4354                }
4355                None => None,
4356            };
4357            let buffer = buffer_store
4358                .update(cx, |buffer_store, cx| {
4359                    buffer_store.create_buffer(git_commit_language, false, cx)
4360                })
4361                .await?;
4362
4363            repository.update(cx, |repository, _| {
4364                repository.commit_message_buffer = Some(buffer.clone());
4365            })?;
4366            Ok(buffer)
4367        })
4368    }
4369
4370    pub fn checkout_files(
4371        &mut self,
4372        commit: &str,
4373        paths: Vec<RepoPath>,
4374        cx: &mut Context<Self>,
4375    ) -> Task<Result<()>> {
4376        let commit = commit.to_string();
4377        let id = self.id;
4378
4379        self.spawn_job_with_tracking(
4380            paths.clone(),
4381            pending_op::GitStatus::Reverted,
4382            cx,
4383            async move |this, cx| {
4384                this.update(cx, |this, _cx| {
4385                    this.send_job(
4386                        Some(format!("git checkout {}", commit).into()),
4387                        move |git_repo, _| async move {
4388                            match git_repo {
4389                                RepositoryState::Local(LocalRepositoryState {
4390                                    backend,
4391                                    environment,
4392                                    ..
4393                                }) => {
4394                                    backend
4395                                        .checkout_files(commit, paths, environment.clone())
4396                                        .await
4397                                }
4398                                RepositoryState::Remote(RemoteRepositoryState {
4399                                    project_id,
4400                                    client,
4401                                }) => {
4402                                    client
4403                                        .request(proto::GitCheckoutFiles {
4404                                            project_id: project_id.0,
4405                                            repository_id: id.to_proto(),
4406                                            commit,
4407                                            paths: paths
4408                                                .into_iter()
4409                                                .map(|p| p.to_proto())
4410                                                .collect(),
4411                                        })
4412                                        .await?;
4413
4414                                    Ok(())
4415                                }
4416                            }
4417                        },
4418                    )
4419                })?
4420                .await?
4421            },
4422        )
4423    }
4424
4425    pub fn reset(
4426        &mut self,
4427        commit: String,
4428        reset_mode: ResetMode,
4429        _cx: &mut App,
4430    ) -> oneshot::Receiver<Result<()>> {
4431        let id = self.id;
4432
4433        self.send_job(None, move |git_repo, _| async move {
4434            match git_repo {
4435                RepositoryState::Local(LocalRepositoryState {
4436                    backend,
4437                    environment,
4438                    ..
4439                }) => backend.reset(commit, reset_mode, environment).await,
4440                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
4441                    client
4442                        .request(proto::GitReset {
4443                            project_id: project_id.0,
4444                            repository_id: id.to_proto(),
4445                            commit,
4446                            mode: match reset_mode {
4447                                ResetMode::Soft => git_reset::ResetMode::Soft.into(),
4448                                ResetMode::Mixed => git_reset::ResetMode::Mixed.into(),
4449                            },
4450                        })
4451                        .await?;
4452
4453                    Ok(())
4454                }
4455            }
4456        })
4457    }
4458
4459    pub fn show(&mut self, commit: String) -> oneshot::Receiver<Result<CommitDetails>> {
4460        let id = self.id;
4461        self.send_job(None, move |git_repo, _cx| async move {
4462            match git_repo {
4463                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
4464                    backend.show(commit).await
4465                }
4466                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
4467                    let resp = client
4468                        .request(proto::GitShow {
4469                            project_id: project_id.0,
4470                            repository_id: id.to_proto(),
4471                            commit,
4472                        })
4473                        .await?;
4474
4475                    Ok(CommitDetails {
4476                        sha: resp.sha.into(),
4477                        message: resp.message.into(),
4478                        commit_timestamp: resp.commit_timestamp,
4479                        author_email: resp.author_email.into(),
4480                        author_name: resp.author_name.into(),
4481                    })
4482                }
4483            }
4484        })
4485    }
4486
4487    pub fn load_commit_diff(&mut self, commit: String) -> oneshot::Receiver<Result<CommitDiff>> {
4488        let id = self.id;
4489        self.send_job(None, move |git_repo, cx| async move {
4490            match git_repo {
4491                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
4492                    backend.load_commit(commit, cx).await
4493                }
4494                RepositoryState::Remote(RemoteRepositoryState {
4495                    client, project_id, ..
4496                }) => {
4497                    let response = client
4498                        .request(proto::LoadCommitDiff {
4499                            project_id: project_id.0,
4500                            repository_id: id.to_proto(),
4501                            commit,
4502                        })
4503                        .await?;
4504                    Ok(CommitDiff {
4505                        files: response
4506                            .files
4507                            .into_iter()
4508                            .map(|file| {
4509                                Ok(CommitFile {
4510                                    path: RepoPath::from_proto(&file.path)?,
4511                                    old_text: file.old_text,
4512                                    new_text: file.new_text,
4513                                    is_binary: file.is_binary,
4514                                })
4515                            })
4516                            .collect::<Result<Vec<_>>>()?,
4517                    })
4518                }
4519            }
4520        })
4521    }
4522
4523    pub fn file_history(
4524        &mut self,
4525        path: RepoPath,
4526    ) -> oneshot::Receiver<Result<git::repository::FileHistory>> {
4527        self.file_history_paginated(path, 0, None)
4528    }
4529
4530    pub fn file_history_paginated(
4531        &mut self,
4532        path: RepoPath,
4533        skip: usize,
4534        limit: Option<usize>,
4535    ) -> oneshot::Receiver<Result<git::repository::FileHistory>> {
4536        let id = self.id;
4537        self.send_job(None, move |git_repo, _cx| async move {
4538            match git_repo {
4539                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
4540                    backend.file_history_paginated(path, skip, limit).await
4541                }
4542                RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => {
4543                    let response = client
4544                        .request(proto::GitFileHistory {
4545                            project_id: project_id.0,
4546                            repository_id: id.to_proto(),
4547                            path: path.to_proto(),
4548                            skip: skip as u64,
4549                            limit: limit.map(|l| l as u64),
4550                        })
4551                        .await?;
4552                    Ok(git::repository::FileHistory {
4553                        entries: response
4554                            .entries
4555                            .into_iter()
4556                            .map(|entry| git::repository::FileHistoryEntry {
4557                                sha: entry.sha.into(),
4558                                subject: entry.subject.into(),
4559                                message: entry.message.into(),
4560                                commit_timestamp: entry.commit_timestamp,
4561                                author_name: entry.author_name.into(),
4562                                author_email: entry.author_email.into(),
4563                            })
4564                            .collect(),
4565                        path: RepoPath::from_proto(&response.path)?,
4566                    })
4567                }
4568            }
4569        })
4570    }
4571
4572    pub fn get_graph_data(
4573        &self,
4574        log_source: LogSource,
4575        log_order: LogOrder,
4576    ) -> Option<&InitialGitGraphData> {
4577        self.initial_graph_data.get(&(log_source, log_order))
4578    }
4579
4580    pub fn search_commits(
4581        &mut self,
4582        log_source: LogSource,
4583        search_args: SearchCommitArgs,
4584        request_tx: smol::channel::Sender<Oid>,
4585        cx: &mut Context<Self>,
4586    ) {
4587        let repository_state = self.repository_state.clone();
4588
4589        cx.background_spawn(async move {
4590            let repo_state = repository_state.await;
4591
4592            match repo_state {
4593                Ok(RepositoryState::Local(LocalRepositoryState { backend, .. })) => {
4594                    backend
4595                        .search_commits(log_source, search_args, request_tx)
4596                        .await
4597                        .log_err();
4598                }
4599                Ok(RepositoryState::Remote(_)) => {}
4600                Err(_) => {}
4601            };
4602        })
4603        .detach();
4604    }
4605
4606    pub fn graph_data(
4607        &mut self,
4608        log_source: LogSource,
4609        log_order: LogOrder,
4610        range: Range<usize>,
4611        cx: &mut Context<Self>,
4612    ) -> GraphDataResponse<'_> {
4613        let initial_commit_data = self
4614            .initial_graph_data
4615            .entry((log_source.clone(), log_order))
4616            .or_insert_with(|| {
4617                let state = self.repository_state.clone();
4618                let log_source = log_source.clone();
4619
4620                let fetch_task = cx.spawn(async move |repository, cx| {
4621                    let state = state.await;
4622                    let result = match state {
4623                        Ok(RepositoryState::Local(LocalRepositoryState { backend, .. })) => {
4624                            Self::local_git_graph_data(
4625                                repository.clone(),
4626                                backend,
4627                                log_source.clone(),
4628                                log_order,
4629                                cx,
4630                            )
4631                            .await
4632                        }
4633                        Ok(RepositoryState::Remote(_)) => {
4634                            Err("Git graph is not supported for collab yet".into())
4635                        }
4636                        Err(e) => Err(SharedString::from(e)),
4637                    };
4638
4639                    if let Err(fetch_task_error) = result {
4640                        repository
4641                            .update(cx, |repository, _| {
4642                                if let Some(data) = repository
4643                                    .initial_graph_data
4644                                    .get_mut(&(log_source, log_order))
4645                                {
4646                                    data.error = Some(fetch_task_error);
4647                                } else {
4648                                    debug_panic!(
4649                                        "This task would be dropped if this entry doesn't exist"
4650                                    );
4651                                }
4652                            })
4653                            .ok();
4654                    }
4655                });
4656
4657                InitialGitGraphData {
4658                    fetch_task,
4659                    error: None,
4660                    commit_data: Vec::new(),
4661                    commit_oid_to_index: HashMap::default(),
4662                }
4663            });
4664
4665        let max_start = initial_commit_data.commit_data.len().saturating_sub(1);
4666        let max_end = initial_commit_data.commit_data.len();
4667
4668        GraphDataResponse {
4669            commits: &initial_commit_data.commit_data
4670                [range.start.min(max_start)..range.end.min(max_end)],
4671            is_loading: !initial_commit_data.fetch_task.is_ready(),
4672            error: initial_commit_data.error.clone(),
4673        }
4674    }
4675
4676    async fn local_git_graph_data(
4677        this: WeakEntity<Self>,
4678        backend: Arc<dyn GitRepository>,
4679        log_source: LogSource,
4680        log_order: LogOrder,
4681        cx: &mut AsyncApp,
4682    ) -> Result<(), SharedString> {
4683        let (request_tx, request_rx) =
4684            smol::channel::unbounded::<Vec<Arc<InitialGraphCommitData>>>();
4685
4686        let task = cx.background_executor().spawn({
4687            let log_source = log_source.clone();
4688            async move {
4689                backend
4690                    .initial_graph_data(log_source, log_order, request_tx)
4691                    .await
4692                    .map_err(|err| SharedString::from(err.to_string()))
4693            }
4694        });
4695
4696        let graph_data_key = (log_source, log_order);
4697
4698        while let Ok(initial_graph_commit_data) = request_rx.recv().await {
4699            this.update(cx, |repository, cx| {
4700                let graph_data = repository
4701                    .initial_graph_data
4702                    .entry(graph_data_key.clone())
4703                    .and_modify(|graph_data| {
4704                        for commit_data in initial_graph_commit_data {
4705                            graph_data
4706                                .commit_oid_to_index
4707                                .insert(commit_data.sha, graph_data.commit_data.len());
4708                            graph_data.commit_data.push(commit_data);
4709
4710                            cx.emit(RepositoryEvent::GraphEvent(
4711                                graph_data_key.clone(),
4712                                GitGraphEvent::CountUpdated(graph_data.commit_data.len()),
4713                            ));
4714                        }
4715                    });
4716
4717                match &graph_data {
4718                    Entry::Occupied(_) => {}
4719                    Entry::Vacant(_) => {
4720                        debug_panic!("This task should be dropped if data doesn't exist");
4721                    }
4722                }
4723            })
4724            .ok();
4725        }
4726
4727        task.await?;
4728        Ok(())
4729    }
4730
4731    pub fn fetch_commit_data(&mut self, sha: Oid, cx: &mut Context<Self>) -> &CommitDataState {
4732        if !self.commit_data.contains_key(&sha) {
4733            match &self.graph_commit_data_handler {
4734                GraphCommitHandlerState::Open(handler) => {
4735                    if handler.commit_data_request.try_send(sha).is_ok() {
4736                        let old_value = self.commit_data.insert(sha, CommitDataState::Loading);
4737                        debug_assert!(old_value.is_none(), "We should never overwrite commit data");
4738                    }
4739                }
4740                GraphCommitHandlerState::Closed => {
4741                    self.open_graph_commit_data_handler(cx);
4742                }
4743                GraphCommitHandlerState::Starting => {}
4744            }
4745        }
4746
4747        self.commit_data
4748            .get(&sha)
4749            .unwrap_or(&CommitDataState::Loading)
4750    }
4751
4752    fn open_graph_commit_data_handler(&mut self, cx: &mut Context<Self>) {
4753        self.graph_commit_data_handler = GraphCommitHandlerState::Starting;
4754
4755        let state = self.repository_state.clone();
4756        let (result_tx, result_rx) = smol::channel::bounded::<(Oid, GraphCommitData)>(64);
4757        let (request_tx, request_rx) = smol::channel::unbounded::<Oid>();
4758
4759        let foreground_task = cx.spawn(async move |this, cx| {
4760            while let Ok((sha, commit_data)) = result_rx.recv().await {
4761                let result = this.update(cx, |this, cx| {
4762                    let old_value = this
4763                        .commit_data
4764                        .insert(sha, CommitDataState::Loaded(Arc::new(commit_data)));
4765                    debug_assert!(
4766                        !matches!(old_value, Some(CommitDataState::Loaded(_))),
4767                        "We should never overwrite commit data"
4768                    );
4769
4770                    cx.notify();
4771                });
4772                if result.is_err() {
4773                    break;
4774                }
4775            }
4776
4777            this.update(cx, |this, _cx| {
4778                this.graph_commit_data_handler = GraphCommitHandlerState::Closed;
4779            })
4780            .ok();
4781        });
4782
4783        let request_tx_for_handler = request_tx;
4784        let background_executor = cx.background_executor().clone();
4785
4786        cx.background_spawn(async move {
4787            let backend = match state.await {
4788                Ok(RepositoryState::Local(LocalRepositoryState { backend, .. })) => backend,
4789                Ok(RepositoryState::Remote(_)) => {
4790                    log::error!("commit_data_reader not supported for remote repositories");
4791                    return;
4792                }
4793                Err(error) => {
4794                    log::error!("failed to get repository state: {error}");
4795                    return;
4796                }
4797            };
4798
4799            let reader = match backend.commit_data_reader() {
4800                Ok(reader) => reader,
4801                Err(error) => {
4802                    log::error!("failed to create commit data reader: {error:?}");
4803                    return;
4804                }
4805            };
4806
4807            loop {
4808                let timeout = background_executor.timer(std::time::Duration::from_secs(10));
4809
4810                futures::select_biased! {
4811                    sha = futures::FutureExt::fuse(request_rx.recv()) => {
4812                        let Ok(sha) = sha else {
4813                            break;
4814                        };
4815
4816                        match reader.read(sha).await {
4817                            Ok(commit_data) => {
4818                                if result_tx.send((sha, commit_data)).await.is_err() {
4819                                    break;
4820                                }
4821                            }
4822                            Err(error) => {
4823                                log::error!("failed to read commit data for {sha}: {error:?}");
4824                            }
4825                        }
4826                    }
4827                    _ = futures::FutureExt::fuse(timeout) => {
4828                        break;
4829                    }
4830                }
4831            }
4832
4833            drop(result_tx);
4834        })
4835        .detach();
4836
4837        self.graph_commit_data_handler = GraphCommitHandlerState::Open(GraphCommitDataHandler {
4838            _task: foreground_task,
4839            commit_data_request: request_tx_for_handler,
4840        });
4841    }
4842
4843    fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
4844        Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
4845    }
4846
4847    fn save_buffers<'a>(
4848        &self,
4849        entries: impl IntoIterator<Item = &'a RepoPath>,
4850        cx: &mut Context<Self>,
4851    ) -> Vec<Task<anyhow::Result<()>>> {
4852        let mut save_futures = Vec::new();
4853        if let Some(buffer_store) = self.buffer_store(cx) {
4854            buffer_store.update(cx, |buffer_store, cx| {
4855                for path in entries {
4856                    let Some(project_path) = self.repo_path_to_project_path(path, cx) else {
4857                        continue;
4858                    };
4859                    if let Some(buffer) = buffer_store.get_by_path(&project_path)
4860                        && buffer
4861                            .read(cx)
4862                            .file()
4863                            .is_some_and(|file| file.disk_state().exists())
4864                        && buffer.read(cx).has_unsaved_edits()
4865                    {
4866                        save_futures.push(buffer_store.save_buffer(buffer, cx));
4867                    }
4868                }
4869            })
4870        }
4871        save_futures
4872    }
4873
4874    pub fn stage_entries(
4875        &mut self,
4876        entries: Vec<RepoPath>,
4877        cx: &mut Context<Self>,
4878    ) -> Task<anyhow::Result<()>> {
4879        self.stage_or_unstage_entries(true, entries, cx)
4880    }
4881
4882    pub fn unstage_entries(
4883        &mut self,
4884        entries: Vec<RepoPath>,
4885        cx: &mut Context<Self>,
4886    ) -> Task<anyhow::Result<()>> {
4887        self.stage_or_unstage_entries(false, entries, cx)
4888    }
4889
4890    fn stage_or_unstage_entries(
4891        &mut self,
4892        stage: bool,
4893        entries: Vec<RepoPath>,
4894        cx: &mut Context<Self>,
4895    ) -> Task<anyhow::Result<()>> {
4896        if entries.is_empty() {
4897            return Task::ready(Ok(()));
4898        }
4899        let Some(git_store) = self.git_store.upgrade() else {
4900            return Task::ready(Ok(()));
4901        };
4902        let id = self.id;
4903        let save_tasks = self.save_buffers(&entries, cx);
4904        let paths = entries
4905            .iter()
4906            .map(|p| p.as_unix_str())
4907            .collect::<Vec<_>>()
4908            .join(" ");
4909        let status = if stage {
4910            format!("git add {paths}")
4911        } else {
4912            format!("git reset {paths}")
4913        };
4914        let job_key = GitJobKey::WriteIndex(entries.clone());
4915
4916        self.spawn_job_with_tracking(
4917            entries.clone(),
4918            if stage {
4919                pending_op::GitStatus::Staged
4920            } else {
4921                pending_op::GitStatus::Unstaged
4922            },
4923            cx,
4924            async move |this, cx| {
4925                for save_task in save_tasks {
4926                    save_task.await?;
4927                }
4928
4929                this.update(cx, |this, cx| {
4930                    let weak_this = cx.weak_entity();
4931                    this.send_keyed_job(
4932                        Some(job_key),
4933                        Some(status.into()),
4934                        move |git_repo, mut cx| async move {
4935                            let hunk_staging_operation_counts = weak_this
4936                                .update(&mut cx, |this, cx| {
4937                                    let mut hunk_staging_operation_counts = HashMap::default();
4938                                    for path in &entries {
4939                                        let Some(project_path) =
4940                                            this.repo_path_to_project_path(path, cx)
4941                                        else {
4942                                            continue;
4943                                        };
4944                                        let Some(buffer) = git_store
4945                                            .read(cx)
4946                                            .buffer_store
4947                                            .read(cx)
4948                                            .get_by_path(&project_path)
4949                                        else {
4950                                            continue;
4951                                        };
4952                                        let Some(diff_state) = git_store
4953                                            .read(cx)
4954                                            .diffs
4955                                            .get(&buffer.read(cx).remote_id())
4956                                            .cloned()
4957                                        else {
4958                                            continue;
4959                                        };
4960                                        let Some(uncommitted_diff) =
4961                                            diff_state.read(cx).uncommitted_diff.as_ref().and_then(
4962                                                |uncommitted_diff| uncommitted_diff.upgrade(),
4963                                            )
4964                                        else {
4965                                            continue;
4966                                        };
4967                                        let buffer_snapshot = buffer.read(cx).text_snapshot();
4968                                        let file_exists = buffer
4969                                            .read(cx)
4970                                            .file()
4971                                            .is_some_and(|file| file.disk_state().exists());
4972                                        let hunk_staging_operation_count =
4973                                            diff_state.update(cx, |diff_state, cx| {
4974                                                uncommitted_diff.update(
4975                                                    cx,
4976                                                    |uncommitted_diff, cx| {
4977                                                        uncommitted_diff
4978                                                            .stage_or_unstage_all_hunks(
4979                                                                stage,
4980                                                                &buffer_snapshot,
4981                                                                file_exists,
4982                                                                cx,
4983                                                            );
4984                                                    },
4985                                                );
4986
4987                                                diff_state.hunk_staging_operation_count += 1;
4988                                                diff_state.hunk_staging_operation_count
4989                                            });
4990                                        hunk_staging_operation_counts.insert(
4991                                            diff_state.downgrade(),
4992                                            hunk_staging_operation_count,
4993                                        );
4994                                    }
4995                                    hunk_staging_operation_counts
4996                                })
4997                                .unwrap_or_default();
4998
4999                            let result = match git_repo {
5000                                RepositoryState::Local(LocalRepositoryState {
5001                                    backend,
5002                                    environment,
5003                                    ..
5004                                }) => {
5005                                    if stage {
5006                                        backend.stage_paths(entries, environment.clone()).await
5007                                    } else {
5008                                        backend.unstage_paths(entries, environment.clone()).await
5009                                    }
5010                                }
5011                                RepositoryState::Remote(RemoteRepositoryState {
5012                                    project_id,
5013                                    client,
5014                                }) => {
5015                                    if stage {
5016                                        client
5017                                            .request(proto::Stage {
5018                                                project_id: project_id.0,
5019                                                repository_id: id.to_proto(),
5020                                                paths: entries
5021                                                    .into_iter()
5022                                                    .map(|repo_path| repo_path.to_proto())
5023                                                    .collect(),
5024                                            })
5025                                            .await
5026                                            .context("sending stage request")
5027                                            .map(|_| ())
5028                                    } else {
5029                                        client
5030                                            .request(proto::Unstage {
5031                                                project_id: project_id.0,
5032                                                repository_id: id.to_proto(),
5033                                                paths: entries
5034                                                    .into_iter()
5035                                                    .map(|repo_path| repo_path.to_proto())
5036                                                    .collect(),
5037                                            })
5038                                            .await
5039                                            .context("sending unstage request")
5040                                            .map(|_| ())
5041                                    }
5042                                }
5043                            };
5044
5045                            for (diff_state, hunk_staging_operation_count) in
5046                                hunk_staging_operation_counts
5047                            {
5048                                diff_state
5049                                    .update(&mut cx, |diff_state, cx| {
5050                                        if result.is_ok() {
5051                                            diff_state.hunk_staging_operation_count_as_of_write =
5052                                                hunk_staging_operation_count;
5053                                        } else if let Some(uncommitted_diff) =
5054                                            &diff_state.uncommitted_diff
5055                                        {
5056                                            uncommitted_diff
5057                                                .update(cx, |uncommitted_diff, cx| {
5058                                                    uncommitted_diff.clear_pending_hunks(cx);
5059                                                })
5060                                                .ok();
5061                                        }
5062                                    })
5063                                    .ok();
5064                            }
5065
5066                            result
5067                        },
5068                    )
5069                })?
5070                .await?
5071            },
5072        )
5073    }
5074
5075    pub fn stage_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
5076        let snapshot = self.snapshot.clone();
5077        let pending_ops = self.pending_ops.clone();
5078        let to_stage = cx.background_spawn(async move {
5079            snapshot
5080                .status()
5081                .filter_map(|entry| {
5082                    if let Some(ops) =
5083                        pending_ops.get(&PathKey(entry.repo_path.as_ref().clone()), ())
5084                    {
5085                        if ops.staging() || ops.staged() {
5086                            None
5087                        } else {
5088                            Some(entry.repo_path)
5089                        }
5090                    } else if entry.status.staging().is_fully_staged() {
5091                        None
5092                    } else {
5093                        Some(entry.repo_path)
5094                    }
5095                })
5096                .collect()
5097        });
5098
5099        cx.spawn(async move |this, cx| {
5100            let to_stage = to_stage.await;
5101            this.update(cx, |this, cx| {
5102                this.stage_or_unstage_entries(true, to_stage, cx)
5103            })?
5104            .await
5105        })
5106    }
5107
5108    pub fn unstage_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
5109        let snapshot = self.snapshot.clone();
5110        let pending_ops = self.pending_ops.clone();
5111        let to_unstage = cx.background_spawn(async move {
5112            snapshot
5113                .status()
5114                .filter_map(|entry| {
5115                    if let Some(ops) =
5116                        pending_ops.get(&PathKey(entry.repo_path.as_ref().clone()), ())
5117                    {
5118                        if !ops.staging() && !ops.staged() {
5119                            None
5120                        } else {
5121                            Some(entry.repo_path)
5122                        }
5123                    } else if entry.status.staging().is_fully_unstaged() {
5124                        None
5125                    } else {
5126                        Some(entry.repo_path)
5127                    }
5128                })
5129                .collect()
5130        });
5131
5132        cx.spawn(async move |this, cx| {
5133            let to_unstage = to_unstage.await;
5134            this.update(cx, |this, cx| {
5135                this.stage_or_unstage_entries(false, to_unstage, cx)
5136            })?
5137            .await
5138        })
5139    }
5140
5141    pub fn stash_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
5142        let to_stash = self.cached_status().map(|entry| entry.repo_path).collect();
5143
5144        self.stash_entries(to_stash, cx)
5145    }
5146
5147    pub fn stash_entries(
5148        &mut self,
5149        entries: Vec<RepoPath>,
5150        cx: &mut Context<Self>,
5151    ) -> Task<anyhow::Result<()>> {
5152        let id = self.id;
5153
5154        cx.spawn(async move |this, cx| {
5155            this.update(cx, |this, _| {
5156                this.send_job(None, move |git_repo, _cx| async move {
5157                    match git_repo {
5158                        RepositoryState::Local(LocalRepositoryState {
5159                            backend,
5160                            environment,
5161                            ..
5162                        }) => backend.stash_paths(entries, environment).await,
5163                        RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5164                            client
5165                                .request(proto::Stash {
5166                                    project_id: project_id.0,
5167                                    repository_id: id.to_proto(),
5168                                    paths: entries
5169                                        .into_iter()
5170                                        .map(|repo_path| repo_path.to_proto())
5171                                        .collect(),
5172                                })
5173                                .await?;
5174                            Ok(())
5175                        }
5176                    }
5177                })
5178            })?
5179            .await??;
5180            Ok(())
5181        })
5182    }
5183
5184    pub fn stash_pop(
5185        &mut self,
5186        index: Option<usize>,
5187        cx: &mut Context<Self>,
5188    ) -> Task<anyhow::Result<()>> {
5189        let id = self.id;
5190        cx.spawn(async move |this, cx| {
5191            this.update(cx, |this, _| {
5192                this.send_job(None, move |git_repo, _cx| async move {
5193                    match git_repo {
5194                        RepositoryState::Local(LocalRepositoryState {
5195                            backend,
5196                            environment,
5197                            ..
5198                        }) => backend.stash_pop(index, environment).await,
5199                        RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5200                            client
5201                                .request(proto::StashPop {
5202                                    project_id: project_id.0,
5203                                    repository_id: id.to_proto(),
5204                                    stash_index: index.map(|i| i as u64),
5205                                })
5206                                .await
5207                                .context("sending stash pop request")?;
5208                            Ok(())
5209                        }
5210                    }
5211                })
5212            })?
5213            .await??;
5214            Ok(())
5215        })
5216    }
5217
5218    pub fn stash_apply(
5219        &mut self,
5220        index: Option<usize>,
5221        cx: &mut Context<Self>,
5222    ) -> Task<anyhow::Result<()>> {
5223        let id = self.id;
5224        cx.spawn(async move |this, cx| {
5225            this.update(cx, |this, _| {
5226                this.send_job(None, move |git_repo, _cx| async move {
5227                    match git_repo {
5228                        RepositoryState::Local(LocalRepositoryState {
5229                            backend,
5230                            environment,
5231                            ..
5232                        }) => backend.stash_apply(index, environment).await,
5233                        RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5234                            client
5235                                .request(proto::StashApply {
5236                                    project_id: project_id.0,
5237                                    repository_id: id.to_proto(),
5238                                    stash_index: index.map(|i| i as u64),
5239                                })
5240                                .await
5241                                .context("sending stash apply request")?;
5242                            Ok(())
5243                        }
5244                    }
5245                })
5246            })?
5247            .await??;
5248            Ok(())
5249        })
5250    }
5251
5252    pub fn stash_drop(
5253        &mut self,
5254        index: Option<usize>,
5255        cx: &mut Context<Self>,
5256    ) -> oneshot::Receiver<anyhow::Result<()>> {
5257        let id = self.id;
5258        let updates_tx = self
5259            .git_store()
5260            .and_then(|git_store| match &git_store.read(cx).state {
5261                GitStoreState::Local { downstream, .. } => downstream
5262                    .as_ref()
5263                    .map(|downstream| downstream.updates_tx.clone()),
5264                _ => None,
5265            });
5266        let this = cx.weak_entity();
5267        self.send_job(None, move |git_repo, mut cx| async move {
5268            match git_repo {
5269                RepositoryState::Local(LocalRepositoryState {
5270                    backend,
5271                    environment,
5272                    ..
5273                }) => {
5274                    // TODO would be nice to not have to do this manually
5275                    let result = backend.stash_drop(index, environment).await;
5276                    if result.is_ok()
5277                        && let Ok(stash_entries) = backend.stash_entries().await
5278                    {
5279                        let snapshot = this.update(&mut cx, |this, cx| {
5280                            this.snapshot.stash_entries = stash_entries;
5281                            cx.emit(RepositoryEvent::StashEntriesChanged);
5282                            this.snapshot.clone()
5283                        })?;
5284                        if let Some(updates_tx) = updates_tx {
5285                            updates_tx
5286                                .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
5287                                .ok();
5288                        }
5289                    }
5290
5291                    result
5292                }
5293                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5294                    client
5295                        .request(proto::StashDrop {
5296                            project_id: project_id.0,
5297                            repository_id: id.to_proto(),
5298                            stash_index: index.map(|i| i as u64),
5299                        })
5300                        .await
5301                        .context("sending stash pop request")?;
5302                    Ok(())
5303                }
5304            }
5305        })
5306    }
5307
5308    pub fn run_hook(&mut self, hook: RunHook, _cx: &mut App) -> oneshot::Receiver<Result<()>> {
5309        let id = self.id;
5310        self.send_job(
5311            Some(format!("git hook {}", hook.as_str()).into()),
5312            move |git_repo, _cx| async move {
5313                match git_repo {
5314                    RepositoryState::Local(LocalRepositoryState {
5315                        backend,
5316                        environment,
5317                        ..
5318                    }) => backend.run_hook(hook, environment.clone()).await,
5319                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5320                        client
5321                            .request(proto::RunGitHook {
5322                                project_id: project_id.0,
5323                                repository_id: id.to_proto(),
5324                                hook: hook.to_proto(),
5325                            })
5326                            .await?;
5327
5328                        Ok(())
5329                    }
5330                }
5331            },
5332        )
5333    }
5334
5335    pub fn commit(
5336        &mut self,
5337        message: SharedString,
5338        name_and_email: Option<(SharedString, SharedString)>,
5339        options: CommitOptions,
5340        askpass: AskPassDelegate,
5341        cx: &mut App,
5342    ) -> oneshot::Receiver<Result<()>> {
5343        let id = self.id;
5344        let askpass_delegates = self.askpass_delegates.clone();
5345        let askpass_id = util::post_inc(&mut self.latest_askpass_id);
5346
5347        let rx = self.run_hook(RunHook::PreCommit, cx);
5348
5349        self.send_job(Some("git commit".into()), move |git_repo, _cx| async move {
5350            rx.await??;
5351
5352            match git_repo {
5353                RepositoryState::Local(LocalRepositoryState {
5354                    backend,
5355                    environment,
5356                    ..
5357                }) => {
5358                    backend
5359                        .commit(message, name_and_email, options, askpass, environment)
5360                        .await
5361                }
5362                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5363                    askpass_delegates.lock().insert(askpass_id, askpass);
5364                    let _defer = util::defer(|| {
5365                        let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
5366                        debug_assert!(askpass_delegate.is_some());
5367                    });
5368                    let (name, email) = name_and_email.unzip();
5369                    client
5370                        .request(proto::Commit {
5371                            project_id: project_id.0,
5372                            repository_id: id.to_proto(),
5373                            message: String::from(message),
5374                            name: name.map(String::from),
5375                            email: email.map(String::from),
5376                            options: Some(proto::commit::CommitOptions {
5377                                amend: options.amend,
5378                                signoff: options.signoff,
5379                                allow_empty: options.allow_empty,
5380                            }),
5381                            askpass_id,
5382                        })
5383                        .await?;
5384
5385                    Ok(())
5386                }
5387            }
5388        })
5389    }
5390
5391    pub fn fetch(
5392        &mut self,
5393        fetch_options: FetchOptions,
5394        askpass: AskPassDelegate,
5395        _cx: &mut App,
5396    ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
5397        let askpass_delegates = self.askpass_delegates.clone();
5398        let askpass_id = util::post_inc(&mut self.latest_askpass_id);
5399        let id = self.id;
5400
5401        self.send_job(Some("git fetch".into()), move |git_repo, cx| async move {
5402            match git_repo {
5403                RepositoryState::Local(LocalRepositoryState {
5404                    backend,
5405                    environment,
5406                    ..
5407                }) => backend.fetch(fetch_options, askpass, environment, cx).await,
5408                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5409                    askpass_delegates.lock().insert(askpass_id, askpass);
5410                    let _defer = util::defer(|| {
5411                        let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
5412                        debug_assert!(askpass_delegate.is_some());
5413                    });
5414
5415                    let response = client
5416                        .request(proto::Fetch {
5417                            project_id: project_id.0,
5418                            repository_id: id.to_proto(),
5419                            askpass_id,
5420                            remote: fetch_options.to_proto(),
5421                        })
5422                        .await?;
5423
5424                    Ok(RemoteCommandOutput {
5425                        stdout: response.stdout,
5426                        stderr: response.stderr,
5427                    })
5428                }
5429            }
5430        })
5431    }
5432
5433    pub fn push(
5434        &mut self,
5435        branch: SharedString,
5436        remote_branch: SharedString,
5437        remote: SharedString,
5438        options: Option<PushOptions>,
5439        askpass: AskPassDelegate,
5440        cx: &mut Context<Self>,
5441    ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
5442        let askpass_delegates = self.askpass_delegates.clone();
5443        let askpass_id = util::post_inc(&mut self.latest_askpass_id);
5444        let id = self.id;
5445
5446        let args = options
5447            .map(|option| match option {
5448                PushOptions::SetUpstream => " --set-upstream",
5449                PushOptions::Force => " --force-with-lease",
5450            })
5451            .unwrap_or("");
5452
5453        let updates_tx = self
5454            .git_store()
5455            .and_then(|git_store| match &git_store.read(cx).state {
5456                GitStoreState::Local { downstream, .. } => downstream
5457                    .as_ref()
5458                    .map(|downstream| downstream.updates_tx.clone()),
5459                _ => None,
5460            });
5461
5462        let this = cx.weak_entity();
5463        self.send_job(
5464            Some(format!("git push {} {} {}:{}", args, remote, branch, remote_branch).into()),
5465            move |git_repo, mut cx| async move {
5466                match git_repo {
5467                    RepositoryState::Local(LocalRepositoryState {
5468                        backend,
5469                        environment,
5470                        ..
5471                    }) => {
5472                        let result = backend
5473                            .push(
5474                                branch.to_string(),
5475                                remote_branch.to_string(),
5476                                remote.to_string(),
5477                                options,
5478                                askpass,
5479                                environment.clone(),
5480                                cx.clone(),
5481                            )
5482                            .await;
5483                        // TODO would be nice to not have to do this manually
5484                        if result.is_ok() {
5485                            let branches = backend.branches().await?;
5486                            let branch = branches.into_iter().find(|branch| branch.is_head);
5487                            log::info!("head branch after scan is {branch:?}");
5488                            let snapshot = this.update(&mut cx, |this, cx| {
5489                                this.snapshot.branch = branch;
5490                                cx.emit(RepositoryEvent::BranchChanged);
5491                                this.snapshot.clone()
5492                            })?;
5493                            if let Some(updates_tx) = updates_tx {
5494                                updates_tx
5495                                    .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
5496                                    .ok();
5497                            }
5498                        }
5499                        result
5500                    }
5501                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5502                        askpass_delegates.lock().insert(askpass_id, askpass);
5503                        let _defer = util::defer(|| {
5504                            let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
5505                            debug_assert!(askpass_delegate.is_some());
5506                        });
5507                        let response = client
5508                            .request(proto::Push {
5509                                project_id: project_id.0,
5510                                repository_id: id.to_proto(),
5511                                askpass_id,
5512                                branch_name: branch.to_string(),
5513                                remote_branch_name: remote_branch.to_string(),
5514                                remote_name: remote.to_string(),
5515                                options: options.map(|options| match options {
5516                                    PushOptions::Force => proto::push::PushOptions::Force,
5517                                    PushOptions::SetUpstream => {
5518                                        proto::push::PushOptions::SetUpstream
5519                                    }
5520                                }
5521                                    as i32),
5522                            })
5523                            .await?;
5524
5525                        Ok(RemoteCommandOutput {
5526                            stdout: response.stdout,
5527                            stderr: response.stderr,
5528                        })
5529                    }
5530                }
5531            },
5532        )
5533    }
5534
5535    pub fn pull(
5536        &mut self,
5537        branch: Option<SharedString>,
5538        remote: SharedString,
5539        rebase: bool,
5540        askpass: AskPassDelegate,
5541        _cx: &mut App,
5542    ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
5543        let askpass_delegates = self.askpass_delegates.clone();
5544        let askpass_id = util::post_inc(&mut self.latest_askpass_id);
5545        let id = self.id;
5546
5547        let mut status = "git pull".to_string();
5548        if rebase {
5549            status.push_str(" --rebase");
5550        }
5551        status.push_str(&format!(" {}", remote));
5552        if let Some(b) = &branch {
5553            status.push_str(&format!(" {}", b));
5554        }
5555
5556        self.send_job(Some(status.into()), move |git_repo, cx| async move {
5557            match git_repo {
5558                RepositoryState::Local(LocalRepositoryState {
5559                    backend,
5560                    environment,
5561                    ..
5562                }) => {
5563                    backend
5564                        .pull(
5565                            branch.as_ref().map(|b| b.to_string()),
5566                            remote.to_string(),
5567                            rebase,
5568                            askpass,
5569                            environment.clone(),
5570                            cx,
5571                        )
5572                        .await
5573                }
5574                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5575                    askpass_delegates.lock().insert(askpass_id, askpass);
5576                    let _defer = util::defer(|| {
5577                        let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
5578                        debug_assert!(askpass_delegate.is_some());
5579                    });
5580                    let response = client
5581                        .request(proto::Pull {
5582                            project_id: project_id.0,
5583                            repository_id: id.to_proto(),
5584                            askpass_id,
5585                            rebase,
5586                            branch_name: branch.as_ref().map(|b| b.to_string()),
5587                            remote_name: remote.to_string(),
5588                        })
5589                        .await?;
5590
5591                    Ok(RemoteCommandOutput {
5592                        stdout: response.stdout,
5593                        stderr: response.stderr,
5594                    })
5595                }
5596            }
5597        })
5598    }
5599
5600    fn spawn_set_index_text_job(
5601        &mut self,
5602        path: RepoPath,
5603        content: Option<String>,
5604        hunk_staging_operation_count: Option<usize>,
5605        cx: &mut Context<Self>,
5606    ) -> oneshot::Receiver<anyhow::Result<()>> {
5607        let id = self.id;
5608        let this = cx.weak_entity();
5609        let git_store = self.git_store.clone();
5610        let abs_path = self.snapshot.repo_path_to_abs_path(&path);
5611        self.send_keyed_job(
5612            Some(GitJobKey::WriteIndex(vec![path.clone()])),
5613            None,
5614            move |git_repo, mut cx| async move {
5615                log::debug!(
5616                    "start updating index text for buffer {}",
5617                    path.as_unix_str()
5618                );
5619
5620                match git_repo {
5621                    RepositoryState::Local(LocalRepositoryState {
5622                        fs,
5623                        backend,
5624                        environment,
5625                        ..
5626                    }) => {
5627                        let executable = match fs.metadata(&abs_path).await {
5628                            Ok(Some(meta)) => meta.is_executable,
5629                            Ok(None) => false,
5630                            Err(_err) => false,
5631                        };
5632                        backend
5633                            .set_index_text(path.clone(), content, environment.clone(), executable)
5634                            .await?;
5635                    }
5636                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5637                        client
5638                            .request(proto::SetIndexText {
5639                                project_id: project_id.0,
5640                                repository_id: id.to_proto(),
5641                                path: path.to_proto(),
5642                                text: content,
5643                            })
5644                            .await?;
5645                    }
5646                }
5647                log::debug!(
5648                    "finish updating index text for buffer {}",
5649                    path.as_unix_str()
5650                );
5651
5652                if let Some(hunk_staging_operation_count) = hunk_staging_operation_count {
5653                    let project_path = this
5654                        .read_with(&cx, |this, cx| this.repo_path_to_project_path(&path, cx))
5655                        .ok()
5656                        .flatten();
5657                    git_store
5658                        .update(&mut cx, |git_store, cx| {
5659                            let buffer_id = git_store
5660                                .buffer_store
5661                                .read(cx)
5662                                .get_by_path(&project_path?)?
5663                                .read(cx)
5664                                .remote_id();
5665                            let diff_state = git_store.diffs.get(&buffer_id)?;
5666                            diff_state.update(cx, |diff_state, _| {
5667                                diff_state.hunk_staging_operation_count_as_of_write =
5668                                    hunk_staging_operation_count;
5669                            });
5670                            Some(())
5671                        })
5672                        .context("Git store dropped")?;
5673                }
5674                Ok(())
5675            },
5676        )
5677    }
5678
5679    pub fn create_remote(
5680        &mut self,
5681        remote_name: String,
5682        remote_url: String,
5683    ) -> oneshot::Receiver<Result<()>> {
5684        let id = self.id;
5685        self.send_job(
5686            Some(format!("git remote add {remote_name} {remote_url}").into()),
5687            move |repo, _cx| async move {
5688                match repo {
5689                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5690                        backend.create_remote(remote_name, remote_url).await
5691                    }
5692                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5693                        client
5694                            .request(proto::GitCreateRemote {
5695                                project_id: project_id.0,
5696                                repository_id: id.to_proto(),
5697                                remote_name,
5698                                remote_url,
5699                            })
5700                            .await?;
5701
5702                        Ok(())
5703                    }
5704                }
5705            },
5706        )
5707    }
5708
5709    pub fn remove_remote(&mut self, remote_name: String) -> oneshot::Receiver<Result<()>> {
5710        let id = self.id;
5711        self.send_job(
5712            Some(format!("git remove remote {remote_name}").into()),
5713            move |repo, _cx| async move {
5714                match repo {
5715                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5716                        backend.remove_remote(remote_name).await
5717                    }
5718                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5719                        client
5720                            .request(proto::GitRemoveRemote {
5721                                project_id: project_id.0,
5722                                repository_id: id.to_proto(),
5723                                remote_name,
5724                            })
5725                            .await?;
5726
5727                        Ok(())
5728                    }
5729                }
5730            },
5731        )
5732    }
5733
5734    pub fn get_remotes(
5735        &mut self,
5736        branch_name: Option<String>,
5737        is_push: bool,
5738    ) -> oneshot::Receiver<Result<Vec<Remote>>> {
5739        let id = self.id;
5740        self.send_job(None, move |repo, _cx| async move {
5741            match repo {
5742                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5743                    let remote = if let Some(branch_name) = branch_name {
5744                        if is_push {
5745                            backend.get_push_remote(branch_name).await?
5746                        } else {
5747                            backend.get_branch_remote(branch_name).await?
5748                        }
5749                    } else {
5750                        None
5751                    };
5752
5753                    match remote {
5754                        Some(remote) => Ok(vec![remote]),
5755                        None => backend.get_all_remotes().await,
5756                    }
5757                }
5758                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5759                    let response = client
5760                        .request(proto::GetRemotes {
5761                            project_id: project_id.0,
5762                            repository_id: id.to_proto(),
5763                            branch_name,
5764                            is_push,
5765                        })
5766                        .await?;
5767
5768                    let remotes = response
5769                        .remotes
5770                        .into_iter()
5771                        .map(|remotes| Remote {
5772                            name: remotes.name.into(),
5773                        })
5774                        .collect();
5775
5776                    Ok(remotes)
5777                }
5778            }
5779        })
5780    }
5781
5782    pub fn branches(&mut self) -> oneshot::Receiver<Result<Vec<Branch>>> {
5783        let id = self.id;
5784        self.send_job(None, move |repo, _| async move {
5785            match repo {
5786                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5787                    backend.branches().await
5788                }
5789                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5790                    let response = client
5791                        .request(proto::GitGetBranches {
5792                            project_id: project_id.0,
5793                            repository_id: id.to_proto(),
5794                        })
5795                        .await?;
5796
5797                    let branches = response
5798                        .branches
5799                        .into_iter()
5800                        .map(|branch| proto_to_branch(&branch))
5801                        .collect();
5802
5803                    Ok(branches)
5804                }
5805            }
5806        })
5807    }
5808
5809    /// If this is a linked worktree (*NOT* the main checkout of a repository),
5810    /// returns the pathed for the linked worktree.
5811    ///
5812    /// Returns None if this is the main checkout.
5813    pub fn linked_worktree_path(&self) -> Option<&Arc<Path>> {
5814        if self.work_directory_abs_path != self.original_repo_abs_path {
5815            Some(&self.work_directory_abs_path)
5816        } else {
5817            None
5818        }
5819    }
5820
5821    pub fn path_for_new_linked_worktree(
5822        &self,
5823        branch_name: &str,
5824        worktree_directory_setting: &str,
5825    ) -> Result<PathBuf> {
5826        let original_repo = self.original_repo_abs_path.clone();
5827        let project_name = original_repo
5828            .file_name()
5829            .ok_or_else(|| anyhow!("git repo must have a directory name"))?;
5830        let directory = worktrees_directory_for_repo(&original_repo, worktree_directory_setting)?;
5831        Ok(directory.join(branch_name).join(project_name))
5832    }
5833
5834    pub fn worktrees(&mut self) -> oneshot::Receiver<Result<Vec<GitWorktree>>> {
5835        let id = self.id;
5836        self.send_job(None, move |repo, _| async move {
5837            match repo {
5838                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5839                    backend.worktrees().await
5840                }
5841                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5842                    let response = client
5843                        .request(proto::GitGetWorktrees {
5844                            project_id: project_id.0,
5845                            repository_id: id.to_proto(),
5846                        })
5847                        .await?;
5848
5849                    let worktrees = response
5850                        .worktrees
5851                        .into_iter()
5852                        .map(|worktree| proto_to_worktree(&worktree))
5853                        .collect();
5854
5855                    Ok(worktrees)
5856                }
5857            }
5858        })
5859    }
5860
5861    pub fn create_worktree(
5862        &mut self,
5863        branch_name: String,
5864        path: PathBuf,
5865        commit: Option<String>,
5866    ) -> oneshot::Receiver<Result<()>> {
5867        let id = self.id;
5868        self.send_job(
5869            Some(format!("git worktree add: {}", branch_name).into()),
5870            move |repo, _cx| async move {
5871                match repo {
5872                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5873                        backend
5874                            .create_worktree(Some(branch_name), path, commit)
5875                            .await
5876                    }
5877                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5878                        client
5879                            .request(proto::GitCreateWorktree {
5880                                project_id: project_id.0,
5881                                repository_id: id.to_proto(),
5882                                name: branch_name,
5883                                directory: path.to_string_lossy().to_string(),
5884                                commit,
5885                            })
5886                            .await?;
5887
5888                        Ok(())
5889                    }
5890                }
5891            },
5892        )
5893    }
5894
5895    pub fn create_worktree_detached(
5896        &mut self,
5897        path: PathBuf,
5898        commit: String,
5899    ) -> oneshot::Receiver<Result<()>> {
5900        self.send_job(
5901            Some("git worktree add (detached)".into()),
5902            move |repo, _cx| async move {
5903                match repo {
5904                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5905                        backend.create_worktree(None, path, Some(commit)).await
5906                    }
5907                    RepositoryState::Remote(_) => {
5908                        anyhow::bail!(
5909                            "create_worktree_detached is not supported for remote repositories"
5910                        )
5911                    }
5912                }
5913            },
5914        )
5915    }
5916
5917    pub fn head_sha(&mut self) -> oneshot::Receiver<Result<Option<String>>> {
5918        self.send_job(None, move |repo, _cx| async move {
5919            match repo {
5920                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5921                    Ok(backend.head_sha().await)
5922                }
5923                RepositoryState::Remote(_) => {
5924                    anyhow::bail!("head_sha is not supported for remote repositories")
5925                }
5926            }
5927        })
5928    }
5929
5930    pub fn update_ref(
5931        &mut self,
5932        ref_name: String,
5933        commit: String,
5934    ) -> oneshot::Receiver<Result<()>> {
5935        self.send_job(None, move |repo, _cx| async move {
5936            match repo {
5937                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5938                    backend.update_ref(ref_name, commit).await
5939                }
5940                RepositoryState::Remote(_) => {
5941                    anyhow::bail!("update_ref is not supported for remote repositories")
5942                }
5943            }
5944        })
5945    }
5946
5947    pub fn delete_ref(&mut self, ref_name: String) -> oneshot::Receiver<Result<()>> {
5948        self.send_job(None, move |repo, _cx| async move {
5949            match repo {
5950                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5951                    backend.delete_ref(ref_name).await
5952                }
5953                RepositoryState::Remote(_) => {
5954                    anyhow::bail!("delete_ref is not supported for remote repositories")
5955                }
5956            }
5957        })
5958    }
5959
5960    pub fn stage_all_including_untracked(&mut self) -> oneshot::Receiver<Result<()>> {
5961        self.send_job(None, move |repo, _cx| async move {
5962            match repo {
5963                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5964                    backend.stage_all_including_untracked().await
5965                }
5966                RepositoryState::Remote(_) => {
5967                    anyhow::bail!(
5968                        "stage_all_including_untracked is not supported for remote repositories"
5969                    )
5970                }
5971            }
5972        })
5973    }
5974
5975    pub fn remove_worktree(&mut self, path: PathBuf, force: bool) -> oneshot::Receiver<Result<()>> {
5976        let id = self.id;
5977        self.send_job(
5978            Some(format!("git worktree remove: {}", path.display()).into()),
5979            move |repo, _cx| async move {
5980                match repo {
5981                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
5982                        backend.remove_worktree(path, force).await
5983                    }
5984                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
5985                        client
5986                            .request(proto::GitRemoveWorktree {
5987                                project_id: project_id.0,
5988                                repository_id: id.to_proto(),
5989                                path: path.to_string_lossy().to_string(),
5990                                force,
5991                            })
5992                            .await?;
5993
5994                        Ok(())
5995                    }
5996                }
5997            },
5998        )
5999    }
6000
6001    pub fn rename_worktree(
6002        &mut self,
6003        old_path: PathBuf,
6004        new_path: PathBuf,
6005    ) -> oneshot::Receiver<Result<()>> {
6006        let id = self.id;
6007        self.send_job(
6008            Some(format!("git worktree move: {}", old_path.display()).into()),
6009            move |repo, _cx| async move {
6010                match repo {
6011                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6012                        backend.rename_worktree(old_path, new_path).await
6013                    }
6014                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6015                        client
6016                            .request(proto::GitRenameWorktree {
6017                                project_id: project_id.0,
6018                                repository_id: id.to_proto(),
6019                                old_path: old_path.to_string_lossy().to_string(),
6020                                new_path: new_path.to_string_lossy().to_string(),
6021                            })
6022                            .await?;
6023
6024                        Ok(())
6025                    }
6026                }
6027            },
6028        )
6029    }
6030
6031    pub fn default_branch(
6032        &mut self,
6033        include_remote_name: bool,
6034    ) -> oneshot::Receiver<Result<Option<SharedString>>> {
6035        let id = self.id;
6036        self.send_job(None, move |repo, _| async move {
6037            match repo {
6038                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6039                    backend.default_branch(include_remote_name).await
6040                }
6041                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6042                    let response = client
6043                        .request(proto::GetDefaultBranch {
6044                            project_id: project_id.0,
6045                            repository_id: id.to_proto(),
6046                        })
6047                        .await?;
6048
6049                    anyhow::Ok(response.branch.map(SharedString::from))
6050                }
6051            }
6052        })
6053    }
6054
6055    pub fn diff_tree(
6056        &mut self,
6057        diff_type: DiffTreeType,
6058        _cx: &App,
6059    ) -> oneshot::Receiver<Result<TreeDiff>> {
6060        let repository_id = self.snapshot.id;
6061        self.send_job(None, move |repo, _cx| async move {
6062            match repo {
6063                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6064                    backend.diff_tree(diff_type).await
6065                }
6066                RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => {
6067                    let response = client
6068                        .request(proto::GetTreeDiff {
6069                            project_id: project_id.0,
6070                            repository_id: repository_id.0,
6071                            is_merge: matches!(diff_type, DiffTreeType::MergeBase { .. }),
6072                            base: diff_type.base().to_string(),
6073                            head: diff_type.head().to_string(),
6074                        })
6075                        .await?;
6076
6077                    let entries = response
6078                        .entries
6079                        .into_iter()
6080                        .filter_map(|entry| {
6081                            let status = match entry.status() {
6082                                proto::tree_diff_status::Status::Added => TreeDiffStatus::Added,
6083                                proto::tree_diff_status::Status::Modified => {
6084                                    TreeDiffStatus::Modified {
6085                                        old: git::Oid::from_str(
6086                                            &entry.oid.context("missing oid").log_err()?,
6087                                        )
6088                                        .log_err()?,
6089                                    }
6090                                }
6091                                proto::tree_diff_status::Status::Deleted => {
6092                                    TreeDiffStatus::Deleted {
6093                                        old: git::Oid::from_str(
6094                                            &entry.oid.context("missing oid").log_err()?,
6095                                        )
6096                                        .log_err()?,
6097                                    }
6098                                }
6099                            };
6100                            Some((
6101                                RepoPath::from_rel_path(
6102                                    &RelPath::from_proto(&entry.path).log_err()?,
6103                                ),
6104                                status,
6105                            ))
6106                        })
6107                        .collect();
6108
6109                    Ok(TreeDiff { entries })
6110                }
6111            }
6112        })
6113    }
6114
6115    pub fn diff(&mut self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver<Result<String>> {
6116        let id = self.id;
6117        self.send_job(None, move |repo, _cx| async move {
6118            match repo {
6119                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6120                    backend.diff(diff_type).await
6121                }
6122                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6123                    let (proto_diff_type, merge_base_ref) = match &diff_type {
6124                        DiffType::HeadToIndex => {
6125                            (proto::git_diff::DiffType::HeadToIndex.into(), None)
6126                        }
6127                        DiffType::HeadToWorktree => {
6128                            (proto::git_diff::DiffType::HeadToWorktree.into(), None)
6129                        }
6130                        DiffType::MergeBase { base_ref } => (
6131                            proto::git_diff::DiffType::MergeBase.into(),
6132                            Some(base_ref.to_string()),
6133                        ),
6134                    };
6135                    let response = client
6136                        .request(proto::GitDiff {
6137                            project_id: project_id.0,
6138                            repository_id: id.to_proto(),
6139                            diff_type: proto_diff_type,
6140                            merge_base_ref,
6141                        })
6142                        .await?;
6143
6144                    Ok(response.diff)
6145                }
6146            }
6147        })
6148    }
6149
6150    pub fn create_branch(
6151        &mut self,
6152        branch_name: String,
6153        base_branch: Option<String>,
6154    ) -> oneshot::Receiver<Result<()>> {
6155        let id = self.id;
6156        let status_msg = if let Some(ref base) = base_branch {
6157            format!("git switch -c {branch_name} {base}").into()
6158        } else {
6159            format!("git switch -c {branch_name}").into()
6160        };
6161        self.send_job(Some(status_msg), move |repo, _cx| async move {
6162            match repo {
6163                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6164                    backend.create_branch(branch_name, base_branch).await
6165                }
6166                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6167                    client
6168                        .request(proto::GitCreateBranch {
6169                            project_id: project_id.0,
6170                            repository_id: id.to_proto(),
6171                            branch_name,
6172                        })
6173                        .await?;
6174
6175                    Ok(())
6176                }
6177            }
6178        })
6179    }
6180
6181    pub fn change_branch(&mut self, branch_name: String) -> oneshot::Receiver<Result<()>> {
6182        let id = self.id;
6183        self.send_job(
6184            Some(format!("git switch {branch_name}").into()),
6185            move |repo, _cx| async move {
6186                match repo {
6187                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6188                        backend.change_branch(branch_name).await
6189                    }
6190                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6191                        client
6192                            .request(proto::GitChangeBranch {
6193                                project_id: project_id.0,
6194                                repository_id: id.to_proto(),
6195                                branch_name,
6196                            })
6197                            .await?;
6198
6199                        Ok(())
6200                    }
6201                }
6202            },
6203        )
6204    }
6205
6206    pub fn delete_branch(
6207        &mut self,
6208        is_remote: bool,
6209        branch_name: String,
6210    ) -> oneshot::Receiver<Result<()>> {
6211        let id = self.id;
6212        self.send_job(
6213            Some(
6214                format!(
6215                    "git branch {} {}",
6216                    if is_remote { "-dr" } else { "-d" },
6217                    branch_name
6218                )
6219                .into(),
6220            ),
6221            move |repo, _cx| async move {
6222                match repo {
6223                    RepositoryState::Local(state) => {
6224                        state.backend.delete_branch(is_remote, branch_name).await
6225                    }
6226                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6227                        client
6228                            .request(proto::GitDeleteBranch {
6229                                project_id: project_id.0,
6230                                repository_id: id.to_proto(),
6231                                is_remote,
6232                                branch_name,
6233                            })
6234                            .await?;
6235
6236                        Ok(())
6237                    }
6238                }
6239            },
6240        )
6241    }
6242
6243    pub fn rename_branch(
6244        &mut self,
6245        branch: String,
6246        new_name: String,
6247    ) -> oneshot::Receiver<Result<()>> {
6248        let id = self.id;
6249        self.send_job(
6250            Some(format!("git branch -m {branch} {new_name}").into()),
6251            move |repo, _cx| async move {
6252                match repo {
6253                    RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6254                        backend.rename_branch(branch, new_name).await
6255                    }
6256                    RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6257                        client
6258                            .request(proto::GitRenameBranch {
6259                                project_id: project_id.0,
6260                                repository_id: id.to_proto(),
6261                                branch,
6262                                new_name,
6263                            })
6264                            .await?;
6265
6266                        Ok(())
6267                    }
6268                }
6269            },
6270        )
6271    }
6272
6273    pub fn check_for_pushed_commits(&mut self) -> oneshot::Receiver<Result<Vec<SharedString>>> {
6274        let id = self.id;
6275        self.send_job(None, move |repo, _cx| async move {
6276            match repo {
6277                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6278                    backend.check_for_pushed_commit().await
6279                }
6280                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6281                    let response = client
6282                        .request(proto::CheckForPushedCommits {
6283                            project_id: project_id.0,
6284                            repository_id: id.to_proto(),
6285                        })
6286                        .await?;
6287
6288                    let branches = response.pushed_to.into_iter().map(Into::into).collect();
6289
6290                    Ok(branches)
6291                }
6292            }
6293        })
6294    }
6295
6296    pub fn checkpoint(&mut self) -> oneshot::Receiver<Result<GitRepositoryCheckpoint>> {
6297        self.send_job(None, |repo, _cx| async move {
6298            match repo {
6299                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6300                    backend.checkpoint().await
6301                }
6302                RepositoryState::Remote(..) => anyhow::bail!("not implemented yet"),
6303            }
6304        })
6305    }
6306
6307    pub fn restore_checkpoint(
6308        &mut self,
6309        checkpoint: GitRepositoryCheckpoint,
6310    ) -> oneshot::Receiver<Result<()>> {
6311        self.send_job(None, move |repo, _cx| async move {
6312            match repo {
6313                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6314                    backend.restore_checkpoint(checkpoint).await
6315                }
6316                RepositoryState::Remote { .. } => anyhow::bail!("not implemented yet"),
6317            }
6318        })
6319    }
6320
6321    pub(crate) fn apply_remote_update(
6322        &mut self,
6323        update: proto::UpdateRepository,
6324        cx: &mut Context<Self>,
6325    ) -> Result<()> {
6326        if let Some(main_path) = &update.original_repo_abs_path {
6327            self.snapshot.original_repo_abs_path = Path::new(main_path.as_str()).into();
6328        }
6329
6330        let new_branch = update.branch_summary.as_ref().map(proto_to_branch);
6331        let new_head_commit = update
6332            .head_commit_details
6333            .as_ref()
6334            .map(proto_to_commit_details);
6335        if self.snapshot.branch != new_branch || self.snapshot.head_commit != new_head_commit {
6336            cx.emit(RepositoryEvent::BranchChanged)
6337        }
6338        self.snapshot.branch = new_branch;
6339        self.snapshot.head_commit = new_head_commit;
6340
6341        // We don't store any merge head state for downstream projects; the upstream
6342        // will track it and we will just get the updated conflicts
6343        let new_merge_heads = TreeMap::from_ordered_entries(
6344            update
6345                .current_merge_conflicts
6346                .into_iter()
6347                .filter_map(|path| Some((RepoPath::from_proto(&path).ok()?, vec![]))),
6348        );
6349        let conflicts_changed =
6350            self.snapshot.merge.merge_heads_by_conflicted_path != new_merge_heads;
6351        self.snapshot.merge.merge_heads_by_conflicted_path = new_merge_heads;
6352        self.snapshot.merge.message = update.merge_message.map(SharedString::from);
6353        let new_stash_entries = GitStash {
6354            entries: update
6355                .stash_entries
6356                .iter()
6357                .filter_map(|entry| proto_to_stash(entry).ok())
6358                .collect(),
6359        };
6360        if self.snapshot.stash_entries != new_stash_entries {
6361            cx.emit(RepositoryEvent::StashEntriesChanged)
6362        }
6363        self.snapshot.stash_entries = new_stash_entries;
6364        let new_linked_worktrees: Arc<[GitWorktree]> = update
6365            .linked_worktrees
6366            .iter()
6367            .map(proto_to_worktree)
6368            .collect();
6369        if *self.snapshot.linked_worktrees != *new_linked_worktrees {
6370            cx.emit(RepositoryEvent::GitWorktreeListChanged);
6371        }
6372        self.snapshot.linked_worktrees = new_linked_worktrees;
6373        self.snapshot.remote_upstream_url = update.remote_upstream_url;
6374        self.snapshot.remote_origin_url = update.remote_origin_url;
6375
6376        let edits = update
6377            .removed_statuses
6378            .into_iter()
6379            .filter_map(|path| {
6380                Some(sum_tree::Edit::Remove(PathKey(
6381                    RelPath::from_proto(&path).log_err()?,
6382                )))
6383            })
6384            .chain(
6385                update
6386                    .updated_statuses
6387                    .into_iter()
6388                    .filter_map(|updated_status| {
6389                        Some(sum_tree::Edit::Insert(updated_status.try_into().log_err()?))
6390                    }),
6391            )
6392            .collect::<Vec<_>>();
6393        if conflicts_changed || !edits.is_empty() {
6394            cx.emit(RepositoryEvent::StatusesChanged);
6395        }
6396        self.snapshot.statuses_by_path.edit(edits, ());
6397
6398        if update.is_last_update {
6399            self.snapshot.scan_id = update.scan_id;
6400        }
6401        self.clear_pending_ops(cx);
6402        Ok(())
6403    }
6404
6405    pub fn compare_checkpoints(
6406        &mut self,
6407        left: GitRepositoryCheckpoint,
6408        right: GitRepositoryCheckpoint,
6409    ) -> oneshot::Receiver<Result<bool>> {
6410        self.send_job(None, move |repo, _cx| async move {
6411            match repo {
6412                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6413                    backend.compare_checkpoints(left, right).await
6414                }
6415                RepositoryState::Remote { .. } => anyhow::bail!("not implemented yet"),
6416            }
6417        })
6418    }
6419
6420    pub fn diff_checkpoints(
6421        &mut self,
6422        base_checkpoint: GitRepositoryCheckpoint,
6423        target_checkpoint: GitRepositoryCheckpoint,
6424    ) -> oneshot::Receiver<Result<String>> {
6425        self.send_job(None, move |repo, _cx| async move {
6426            match repo {
6427                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6428                    backend
6429                        .diff_checkpoints(base_checkpoint, target_checkpoint)
6430                        .await
6431                }
6432                RepositoryState::Remote { .. } => anyhow::bail!("not implemented yet"),
6433            }
6434        })
6435    }
6436
6437    fn clear_pending_ops(&mut self, cx: &mut Context<Self>) {
6438        let updated = SumTree::from_iter(
6439            self.pending_ops.iter().filter_map(|ops| {
6440                let inner_ops: Vec<PendingOp> =
6441                    ops.ops.iter().filter(|op| op.running()).cloned().collect();
6442                if inner_ops.is_empty() {
6443                    None
6444                } else {
6445                    Some(PendingOps {
6446                        repo_path: ops.repo_path.clone(),
6447                        ops: inner_ops,
6448                    })
6449                }
6450            }),
6451            (),
6452        );
6453
6454        if updated != self.pending_ops {
6455            cx.emit(RepositoryEvent::PendingOpsChanged {
6456                pending_ops: self.pending_ops.clone(),
6457            })
6458        }
6459
6460        self.pending_ops = updated;
6461    }
6462
6463    fn schedule_scan(
6464        &mut self,
6465        updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
6466        cx: &mut Context<Self>,
6467    ) {
6468        let this = cx.weak_entity();
6469        let _ = self.send_keyed_job(
6470            Some(GitJobKey::ReloadGitState),
6471            None,
6472            |state, mut cx| async move {
6473                log::debug!("run scheduled git status scan");
6474
6475                let Some(this) = this.upgrade() else {
6476                    return Ok(());
6477                };
6478                let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else {
6479                    bail!("not a local repository")
6480                };
6481                let snapshot = compute_snapshot(this.clone(), backend.clone(), &mut cx).await?;
6482                this.update(&mut cx, |this, cx| {
6483                    this.clear_pending_ops(cx);
6484                });
6485                if let Some(updates_tx) = updates_tx {
6486                    updates_tx
6487                        .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
6488                        .ok();
6489                }
6490                Ok(())
6491            },
6492        );
6493    }
6494
6495    fn spawn_local_git_worker(
6496        state: Shared<Task<Result<LocalRepositoryState, String>>>,
6497        cx: &mut Context<Self>,
6498    ) -> mpsc::UnboundedSender<GitJob> {
6499        let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
6500
6501        cx.spawn(async move |_, cx| {
6502            let state = state.await.map_err(|err| anyhow::anyhow!(err))?;
6503            if let Some(git_hosting_provider_registry) =
6504                cx.update(|cx| GitHostingProviderRegistry::try_global(cx))
6505            {
6506                git_hosting_providers::register_additional_providers(
6507                    git_hosting_provider_registry,
6508                    state.backend.clone(),
6509                )
6510                .await;
6511            }
6512            let state = RepositoryState::Local(state);
6513            let mut jobs = VecDeque::new();
6514            loop {
6515                while let Ok(Some(next_job)) = job_rx.try_next() {
6516                    jobs.push_back(next_job);
6517                }
6518
6519                if let Some(job) = jobs.pop_front() {
6520                    if let Some(current_key) = &job.key
6521                        && jobs
6522                            .iter()
6523                            .any(|other_job| other_job.key.as_ref() == Some(current_key))
6524                    {
6525                        continue;
6526                    }
6527                    (job.job)(state.clone(), cx).await;
6528                } else if let Some(job) = job_rx.next().await {
6529                    jobs.push_back(job);
6530                } else {
6531                    break;
6532                }
6533            }
6534            anyhow::Ok(())
6535        })
6536        .detach_and_log_err(cx);
6537
6538        job_tx
6539    }
6540
6541    fn spawn_remote_git_worker(
6542        state: RemoteRepositoryState,
6543        cx: &mut Context<Self>,
6544    ) -> mpsc::UnboundedSender<GitJob> {
6545        let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
6546
6547        cx.spawn(async move |_, cx| {
6548            let state = RepositoryState::Remote(state);
6549            let mut jobs = VecDeque::new();
6550            loop {
6551                while let Ok(Some(next_job)) = job_rx.try_next() {
6552                    jobs.push_back(next_job);
6553                }
6554
6555                if let Some(job) = jobs.pop_front() {
6556                    if let Some(current_key) = &job.key
6557                        && jobs
6558                            .iter()
6559                            .any(|other_job| other_job.key.as_ref() == Some(current_key))
6560                    {
6561                        continue;
6562                    }
6563                    (job.job)(state.clone(), cx).await;
6564                } else if let Some(job) = job_rx.next().await {
6565                    jobs.push_back(job);
6566                } else {
6567                    break;
6568                }
6569            }
6570            anyhow::Ok(())
6571        })
6572        .detach_and_log_err(cx);
6573
6574        job_tx
6575    }
6576
6577    fn load_staged_text(
6578        &mut self,
6579        buffer_id: BufferId,
6580        repo_path: RepoPath,
6581        cx: &App,
6582    ) -> Task<Result<Option<String>>> {
6583        let rx = self.send_job(None, move |state, _| async move {
6584            match state {
6585                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6586                    anyhow::Ok(backend.load_index_text(repo_path).await)
6587                }
6588                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6589                    let response = client
6590                        .request(proto::OpenUnstagedDiff {
6591                            project_id: project_id.to_proto(),
6592                            buffer_id: buffer_id.to_proto(),
6593                        })
6594                        .await?;
6595                    Ok(response.staged_text)
6596                }
6597            }
6598        });
6599        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
6600    }
6601
6602    fn load_committed_text(
6603        &mut self,
6604        buffer_id: BufferId,
6605        repo_path: RepoPath,
6606        cx: &App,
6607    ) -> Task<Result<DiffBasesChange>> {
6608        let rx = self.send_job(None, move |state, _| async move {
6609            match state {
6610                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6611                    let committed_text = backend.load_committed_text(repo_path.clone()).await;
6612                    let staged_text = backend.load_index_text(repo_path).await;
6613                    let diff_bases_change = if committed_text == staged_text {
6614                        DiffBasesChange::SetBoth(committed_text)
6615                    } else {
6616                        DiffBasesChange::SetEach {
6617                            index: staged_text,
6618                            head: committed_text,
6619                        }
6620                    };
6621                    anyhow::Ok(diff_bases_change)
6622                }
6623                RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
6624                    use proto::open_uncommitted_diff_response::Mode;
6625
6626                    let response = client
6627                        .request(proto::OpenUncommittedDiff {
6628                            project_id: project_id.to_proto(),
6629                            buffer_id: buffer_id.to_proto(),
6630                        })
6631                        .await?;
6632                    let mode = Mode::from_i32(response.mode).context("Invalid mode")?;
6633                    let bases = match mode {
6634                        Mode::IndexMatchesHead => DiffBasesChange::SetBoth(response.committed_text),
6635                        Mode::IndexAndHead => DiffBasesChange::SetEach {
6636                            head: response.committed_text,
6637                            index: response.staged_text,
6638                        },
6639                    };
6640                    Ok(bases)
6641                }
6642            }
6643        });
6644
6645        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
6646    }
6647
6648    fn load_blob_content(&mut self, oid: Oid, cx: &App) -> Task<Result<String>> {
6649        let repository_id = self.snapshot.id;
6650        let rx = self.send_job(None, move |state, _| async move {
6651            match state {
6652                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
6653                    backend.load_blob_content(oid).await
6654                }
6655                RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => {
6656                    let response = client
6657                        .request(proto::GetBlobContent {
6658                            project_id: project_id.to_proto(),
6659                            repository_id: repository_id.0,
6660                            oid: oid.to_string(),
6661                        })
6662                        .await?;
6663                    Ok(response.content)
6664                }
6665            }
6666        });
6667        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
6668    }
6669
6670    fn paths_changed(
6671        &mut self,
6672        paths: Vec<RepoPath>,
6673        updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
6674        cx: &mut Context<Self>,
6675    ) {
6676        if !paths.is_empty() {
6677            self.paths_needing_status_update.push(paths);
6678        }
6679
6680        let this = cx.weak_entity();
6681        let _ = self.send_keyed_job(
6682            Some(GitJobKey::RefreshStatuses),
6683            None,
6684            |state, mut cx| async move {
6685                let (prev_snapshot, changed_paths) = this.update(&mut cx, |this, _| {
6686                    (
6687                        this.snapshot.clone(),
6688                        mem::take(&mut this.paths_needing_status_update),
6689                    )
6690                })?;
6691                let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else {
6692                    bail!("not a local repository")
6693                };
6694
6695                if changed_paths.is_empty() {
6696                    return Ok(());
6697                }
6698
6699                let has_head = prev_snapshot.head_commit.is_some();
6700
6701                let stash_entries = backend.stash_entries().await?;
6702                let changed_path_statuses = cx
6703                    .background_spawn(async move {
6704                        let mut changed_paths =
6705                            changed_paths.into_iter().flatten().collect::<BTreeSet<_>>();
6706                        let changed_paths_vec = changed_paths.iter().cloned().collect::<Vec<_>>();
6707
6708                        let status_task = backend.status(&changed_paths_vec);
6709                        let diff_stat_future = if has_head {
6710                            backend.diff_stat(&changed_paths_vec)
6711                        } else {
6712                            future::ready(Ok(status::GitDiffStat {
6713                                entries: Arc::default(),
6714                            }))
6715                            .boxed()
6716                        };
6717
6718                        let (statuses, diff_stats) =
6719                            futures::future::try_join(status_task, diff_stat_future).await?;
6720
6721                        let diff_stats: HashMap<RepoPath, DiffStat> =
6722                            HashMap::from_iter(diff_stats.entries.into_iter().cloned());
6723
6724                        let mut changed_path_statuses = Vec::new();
6725                        let prev_statuses = prev_snapshot.statuses_by_path.clone();
6726                        let mut cursor = prev_statuses.cursor::<PathProgress>(());
6727
6728                        for (repo_path, status) in &*statuses.entries {
6729                            let current_diff_stat = diff_stats.get(repo_path).copied();
6730
6731                            changed_paths.remove(repo_path);
6732                            if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left)
6733                                && cursor.item().is_some_and(|entry| {
6734                                    entry.status == *status && entry.diff_stat == current_diff_stat
6735                                })
6736                            {
6737                                continue;
6738                            }
6739
6740                            changed_path_statuses.push(Edit::Insert(StatusEntry {
6741                                repo_path: repo_path.clone(),
6742                                status: *status,
6743                                diff_stat: current_diff_stat,
6744                            }));
6745                        }
6746                        let mut cursor = prev_statuses.cursor::<PathProgress>(());
6747                        for path in changed_paths.into_iter() {
6748                            if cursor.seek_forward(&PathTarget::Path(&path), Bias::Left) {
6749                                changed_path_statuses
6750                                    .push(Edit::Remove(PathKey(path.as_ref().clone())));
6751                            }
6752                        }
6753                        anyhow::Ok(changed_path_statuses)
6754                    })
6755                    .await?;
6756
6757                this.update(&mut cx, |this, cx| {
6758                    if this.snapshot.stash_entries != stash_entries {
6759                        cx.emit(RepositoryEvent::StashEntriesChanged);
6760                        this.snapshot.stash_entries = stash_entries;
6761                    }
6762
6763                    if !changed_path_statuses.is_empty() {
6764                        cx.emit(RepositoryEvent::StatusesChanged);
6765                        this.snapshot
6766                            .statuses_by_path
6767                            .edit(changed_path_statuses, ());
6768                        this.snapshot.scan_id += 1;
6769                    }
6770
6771                    if let Some(updates_tx) = updates_tx {
6772                        updates_tx
6773                            .unbounded_send(DownstreamUpdate::UpdateRepository(
6774                                this.snapshot.clone(),
6775                            ))
6776                            .ok();
6777                    }
6778                })
6779            },
6780        );
6781    }
6782
6783    /// currently running git command and when it started
6784    pub fn current_job(&self) -> Option<JobInfo> {
6785        self.active_jobs.values().next().cloned()
6786    }
6787
6788    pub fn barrier(&mut self) -> oneshot::Receiver<()> {
6789        self.send_job(None, |_, _| async {})
6790    }
6791
6792    fn spawn_job_with_tracking<AsyncFn>(
6793        &mut self,
6794        paths: Vec<RepoPath>,
6795        git_status: pending_op::GitStatus,
6796        cx: &mut Context<Self>,
6797        f: AsyncFn,
6798    ) -> Task<Result<()>>
6799    where
6800        AsyncFn: AsyncFnOnce(WeakEntity<Repository>, &mut AsyncApp) -> Result<()> + 'static,
6801    {
6802        let ids = self.new_pending_ops_for_paths(paths, git_status);
6803
6804        cx.spawn(async move |this, cx| {
6805            let (job_status, result) = match f(this.clone(), cx).await {
6806                Ok(()) => (pending_op::JobStatus::Finished, Ok(())),
6807                Err(err) if err.is::<Canceled>() => (pending_op::JobStatus::Skipped, Ok(())),
6808                Err(err) => (pending_op::JobStatus::Error, Err(err)),
6809            };
6810
6811            this.update(cx, |this, _| {
6812                let mut edits = Vec::with_capacity(ids.len());
6813                for (id, entry) in ids {
6814                    if let Some(mut ops) = this
6815                        .pending_ops
6816                        .get(&PathKey(entry.as_ref().clone()), ())
6817                        .cloned()
6818                    {
6819                        if let Some(op) = ops.op_by_id_mut(id) {
6820                            op.job_status = job_status;
6821                        }
6822                        edits.push(sum_tree::Edit::Insert(ops));
6823                    }
6824                }
6825                this.pending_ops.edit(edits, ());
6826            })?;
6827
6828            result
6829        })
6830    }
6831
6832    fn new_pending_ops_for_paths(
6833        &mut self,
6834        paths: Vec<RepoPath>,
6835        git_status: pending_op::GitStatus,
6836    ) -> Vec<(PendingOpId, RepoPath)> {
6837        let mut edits = Vec::with_capacity(paths.len());
6838        let mut ids = Vec::with_capacity(paths.len());
6839        for path in paths {
6840            let mut ops = self
6841                .pending_ops
6842                .get(&PathKey(path.as_ref().clone()), ())
6843                .cloned()
6844                .unwrap_or_else(|| PendingOps::new(&path));
6845            let id = ops.max_id() + 1;
6846            ops.ops.push(PendingOp {
6847                id,
6848                git_status,
6849                job_status: pending_op::JobStatus::Running,
6850            });
6851            edits.push(sum_tree::Edit::Insert(ops));
6852            ids.push((id, path));
6853        }
6854        self.pending_ops.edit(edits, ());
6855        ids
6856    }
6857    pub fn default_remote_url(&self) -> Option<String> {
6858        self.remote_upstream_url
6859            .clone()
6860            .or(self.remote_origin_url.clone())
6861    }
6862}
6863
6864/// If `path` is a git linked worktree checkout, resolves it to the main
6865/// repository's working directory path. Returns `None` if `path` is a normal
6866/// repository, not a git repo, or if resolution fails.
6867///
6868/// Resolution works by:
6869/// 1. Reading the `.git` file to get the `gitdir:` pointer
6870/// 2. Following that to the worktree-specific git directory
6871/// 3. Reading the `commondir` file to find the shared `.git` directory
6872/// 4. Deriving the main repo's working directory from the common dir
6873pub async fn resolve_git_worktree_to_main_repo(fs: &dyn Fs, path: &Path) -> Option<PathBuf> {
6874    let dot_git = path.join(".git");
6875    let metadata = fs.metadata(&dot_git).await.ok()??;
6876    if metadata.is_dir {
6877        return None; // Normal repo, not a linked worktree
6878    }
6879    // It's a .git file — parse the gitdir: pointer
6880    let content = fs.load(&dot_git).await.ok()?;
6881    let gitdir_rel = content.strip_prefix("gitdir:")?.trim();
6882    let gitdir_abs = fs.canonicalize(&path.join(gitdir_rel)).await.ok()?;
6883    // Read commondir to find the main .git directory
6884    let commondir_content = fs.load(&gitdir_abs.join("commondir")).await.ok()?;
6885    let common_dir = fs
6886        .canonicalize(&gitdir_abs.join(commondir_content.trim()))
6887        .await
6888        .ok()?;
6889    Some(git::repository::original_repo_path_from_common_dir(
6890        &common_dir,
6891    ))
6892}
6893
6894/// Validates that the resolved worktree directory is acceptable:
6895/// - The setting must not be an absolute path.
6896/// - The resolved path must be either a subdirectory of the working
6897///   directory or a subdirectory of its parent (i.e., a sibling).
6898///
6899/// Returns `Ok(resolved_path)` or an error with a user-facing message.
6900pub fn worktrees_directory_for_repo(
6901    original_repo_abs_path: &Path,
6902    worktree_directory_setting: &str,
6903) -> Result<PathBuf> {
6904    // Check the original setting before trimming, since a path like "///"
6905    // is absolute but becomes "" after stripping trailing separators.
6906    // Also check for leading `/` or `\` explicitly, because on Windows
6907    // `Path::is_absolute()` requires a drive letter — so `/tmp/worktrees`
6908    // would slip through even though it's clearly not a relative path.
6909    if Path::new(worktree_directory_setting).is_absolute()
6910        || worktree_directory_setting.starts_with('/')
6911        || worktree_directory_setting.starts_with('\\')
6912    {
6913        anyhow::bail!(
6914            "git.worktree_directory must be a relative path, got: {worktree_directory_setting:?}"
6915        );
6916    }
6917
6918    if worktree_directory_setting.is_empty() {
6919        anyhow::bail!("git.worktree_directory must not be empty");
6920    }
6921
6922    let trimmed = worktree_directory_setting.trim_end_matches(['/', '\\']);
6923    if trimmed == ".." {
6924        anyhow::bail!("git.worktree_directory must not be \"..\" (use \"../some-name\" instead)");
6925    }
6926
6927    let joined = original_repo_abs_path.join(trimmed);
6928    let resolved = util::normalize_path(&joined);
6929    let resolved = if resolved.starts_with(original_repo_abs_path) {
6930        resolved
6931    } else if let Some(repo_dir_name) = original_repo_abs_path.file_name() {
6932        resolved.join(repo_dir_name)
6933    } else {
6934        resolved
6935    };
6936
6937    let parent = original_repo_abs_path
6938        .parent()
6939        .unwrap_or(original_repo_abs_path);
6940
6941    if !resolved.starts_with(parent) {
6942        anyhow::bail!(
6943            "git.worktree_directory resolved to {resolved:?}, which is outside \
6944             the project root and its parent directory. It must resolve to a \
6945             subdirectory of {original_repo_abs_path:?} or a sibling of it."
6946        );
6947    }
6948
6949    Ok(resolved)
6950}
6951
6952/// Returns a short name for a linked worktree suitable for UI display
6953///
6954/// Uses the main worktree path to come up with a short name that disambiguates
6955/// the linked worktree from the main worktree.
6956pub fn linked_worktree_short_name(
6957    main_worktree_path: &Path,
6958    linked_worktree_path: &Path,
6959) -> Option<SharedString> {
6960    if main_worktree_path == linked_worktree_path {
6961        return None;
6962    }
6963
6964    let project_name = main_worktree_path.file_name()?.to_str()?;
6965    let directory_name = linked_worktree_path.file_name()?.to_str()?;
6966    let name = if directory_name != project_name {
6967        directory_name.to_string()
6968    } else {
6969        linked_worktree_path
6970            .parent()?
6971            .file_name()?
6972            .to_str()?
6973            .to_string()
6974    };
6975    Some(name.into())
6976}
6977
6978fn get_permalink_in_rust_registry_src(
6979    provider_registry: Arc<GitHostingProviderRegistry>,
6980    path: PathBuf,
6981    selection: Range<u32>,
6982) -> Result<url::Url> {
6983    #[derive(Deserialize)]
6984    struct CargoVcsGit {
6985        sha1: String,
6986    }
6987
6988    #[derive(Deserialize)]
6989    struct CargoVcsInfo {
6990        git: CargoVcsGit,
6991        path_in_vcs: String,
6992    }
6993
6994    #[derive(Deserialize)]
6995    struct CargoPackage {
6996        repository: String,
6997    }
6998
6999    #[derive(Deserialize)]
7000    struct CargoToml {
7001        package: CargoPackage,
7002    }
7003
7004    let Some((dir, cargo_vcs_info_json)) = path.ancestors().skip(1).find_map(|dir| {
7005        let json = std::fs::read_to_string(dir.join(".cargo_vcs_info.json")).ok()?;
7006        Some((dir, json))
7007    }) else {
7008        bail!("No .cargo_vcs_info.json found in parent directories")
7009    };
7010    let cargo_vcs_info = serde_json::from_str::<CargoVcsInfo>(&cargo_vcs_info_json)?;
7011    let cargo_toml = std::fs::read_to_string(dir.join("Cargo.toml"))?;
7012    let manifest = toml::from_str::<CargoToml>(&cargo_toml)?;
7013    let (provider, remote) = parse_git_remote_url(provider_registry, &manifest.package.repository)
7014        .context("parsing package.repository field of manifest")?;
7015    let path = PathBuf::from(cargo_vcs_info.path_in_vcs).join(path.strip_prefix(dir).unwrap());
7016    let permalink = provider.build_permalink(
7017        remote,
7018        BuildPermalinkParams::new(
7019            &cargo_vcs_info.git.sha1,
7020            &RepoPath::from_rel_path(
7021                &RelPath::new(&path, PathStyle::local()).context("invalid path")?,
7022            ),
7023            Some(selection),
7024        ),
7025    );
7026    Ok(permalink)
7027}
7028
7029fn serialize_blame_buffer_response(blame: Option<git::blame::Blame>) -> proto::BlameBufferResponse {
7030    let Some(blame) = blame else {
7031        return proto::BlameBufferResponse {
7032            blame_response: None,
7033        };
7034    };
7035
7036    let entries = blame
7037        .entries
7038        .into_iter()
7039        .map(|entry| proto::BlameEntry {
7040            sha: entry.sha.as_bytes().into(),
7041            start_line: entry.range.start,
7042            end_line: entry.range.end,
7043            original_line_number: entry.original_line_number,
7044            author: entry.author,
7045            author_mail: entry.author_mail,
7046            author_time: entry.author_time,
7047            author_tz: entry.author_tz,
7048            committer: entry.committer_name,
7049            committer_mail: entry.committer_email,
7050            committer_time: entry.committer_time,
7051            committer_tz: entry.committer_tz,
7052            summary: entry.summary,
7053            previous: entry.previous,
7054            filename: entry.filename,
7055        })
7056        .collect::<Vec<_>>();
7057
7058    let messages = blame
7059        .messages
7060        .into_iter()
7061        .map(|(oid, message)| proto::CommitMessage {
7062            oid: oid.as_bytes().into(),
7063            message,
7064        })
7065        .collect::<Vec<_>>();
7066
7067    proto::BlameBufferResponse {
7068        blame_response: Some(proto::blame_buffer_response::BlameResponse { entries, messages }),
7069    }
7070}
7071
7072fn deserialize_blame_buffer_response(
7073    response: proto::BlameBufferResponse,
7074) -> Option<git::blame::Blame> {
7075    let response = response.blame_response?;
7076    let entries = response
7077        .entries
7078        .into_iter()
7079        .filter_map(|entry| {
7080            Some(git::blame::BlameEntry {
7081                sha: git::Oid::from_bytes(&entry.sha).ok()?,
7082                range: entry.start_line..entry.end_line,
7083                original_line_number: entry.original_line_number,
7084                committer_name: entry.committer,
7085                committer_time: entry.committer_time,
7086                committer_tz: entry.committer_tz,
7087                committer_email: entry.committer_mail,
7088                author: entry.author,
7089                author_mail: entry.author_mail,
7090                author_time: entry.author_time,
7091                author_tz: entry.author_tz,
7092                summary: entry.summary,
7093                previous: entry.previous,
7094                filename: entry.filename,
7095            })
7096        })
7097        .collect::<Vec<_>>();
7098
7099    let messages = response
7100        .messages
7101        .into_iter()
7102        .filter_map(|message| Some((git::Oid::from_bytes(&message.oid).ok()?, message.message)))
7103        .collect::<HashMap<_, _>>();
7104
7105    Some(Blame { entries, messages })
7106}
7107
7108fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch {
7109    proto::Branch {
7110        is_head: branch.is_head,
7111        ref_name: branch.ref_name.to_string(),
7112        unix_timestamp: branch
7113            .most_recent_commit
7114            .as_ref()
7115            .map(|commit| commit.commit_timestamp as u64),
7116        upstream: branch.upstream.as_ref().map(|upstream| proto::GitUpstream {
7117            ref_name: upstream.ref_name.to_string(),
7118            tracking: upstream
7119                .tracking
7120                .status()
7121                .map(|upstream| proto::UpstreamTracking {
7122                    ahead: upstream.ahead as u64,
7123                    behind: upstream.behind as u64,
7124                }),
7125        }),
7126        most_recent_commit: branch
7127            .most_recent_commit
7128            .as_ref()
7129            .map(|commit| proto::CommitSummary {
7130                sha: commit.sha.to_string(),
7131                subject: commit.subject.to_string(),
7132                commit_timestamp: commit.commit_timestamp,
7133                author_name: commit.author_name.to_string(),
7134            }),
7135    }
7136}
7137
7138fn worktree_to_proto(worktree: &git::repository::Worktree) -> proto::Worktree {
7139    proto::Worktree {
7140        path: worktree.path.to_string_lossy().to_string(),
7141        ref_name: worktree
7142            .ref_name
7143            .as_ref()
7144            .map(|s| s.to_string())
7145            .unwrap_or_default(),
7146        sha: worktree.sha.to_string(),
7147        is_main: worktree.is_main,
7148    }
7149}
7150
7151fn proto_to_worktree(proto: &proto::Worktree) -> git::repository::Worktree {
7152    git::repository::Worktree {
7153        path: PathBuf::from(proto.path.clone()),
7154        ref_name: Some(SharedString::from(&proto.ref_name)),
7155        sha: proto.sha.clone().into(),
7156        is_main: proto.is_main,
7157    }
7158}
7159
7160fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {
7161    git::repository::Branch {
7162        is_head: proto.is_head,
7163        ref_name: proto.ref_name.clone().into(),
7164        upstream: proto
7165            .upstream
7166            .as_ref()
7167            .map(|upstream| git::repository::Upstream {
7168                ref_name: upstream.ref_name.to_string().into(),
7169                tracking: upstream
7170                    .tracking
7171                    .as_ref()
7172                    .map(|tracking| {
7173                        git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus {
7174                            ahead: tracking.ahead as u32,
7175                            behind: tracking.behind as u32,
7176                        })
7177                    })
7178                    .unwrap_or(git::repository::UpstreamTracking::Gone),
7179            }),
7180        most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| {
7181            git::repository::CommitSummary {
7182                sha: commit.sha.to_string().into(),
7183                subject: commit.subject.to_string().into(),
7184                commit_timestamp: commit.commit_timestamp,
7185                author_name: commit.author_name.to_string().into(),
7186                has_parent: true,
7187            }
7188        }),
7189    }
7190}
7191
7192fn commit_details_to_proto(commit: &CommitDetails) -> proto::GitCommitDetails {
7193    proto::GitCommitDetails {
7194        sha: commit.sha.to_string(),
7195        message: commit.message.to_string(),
7196        commit_timestamp: commit.commit_timestamp,
7197        author_email: commit.author_email.to_string(),
7198        author_name: commit.author_name.to_string(),
7199    }
7200}
7201
7202fn proto_to_commit_details(proto: &proto::GitCommitDetails) -> CommitDetails {
7203    CommitDetails {
7204        sha: proto.sha.clone().into(),
7205        message: proto.message.clone().into(),
7206        commit_timestamp: proto.commit_timestamp,
7207        author_email: proto.author_email.clone().into(),
7208        author_name: proto.author_name.clone().into(),
7209    }
7210}
7211
7212/// This snapshot computes the repository state on the foreground thread while
7213/// running the git commands on the background thread. We update branch, head,
7214/// remotes, and worktrees first so the UI can react sooner, then compute file
7215/// state and emit those events immediately after.
7216async fn compute_snapshot(
7217    this: Entity<Repository>,
7218    backend: Arc<dyn GitRepository>,
7219    cx: &mut AsyncApp,
7220) -> Result<RepositorySnapshot> {
7221    let (id, work_directory_abs_path, prev_snapshot) = this.update(cx, |this, _| {
7222        this.paths_needing_status_update.clear();
7223        (
7224            this.id,
7225            this.work_directory_abs_path.clone(),
7226            this.snapshot.clone(),
7227        )
7228    });
7229
7230    let head_commit_future = {
7231        let backend = backend.clone();
7232        async move {
7233            Ok(match backend.head_sha().await {
7234                Some(head_sha) => backend.show(head_sha).await.log_err(),
7235                None => None,
7236            })
7237        }
7238    };
7239    let (branches, head_commit, all_worktrees) = cx
7240        .background_spawn({
7241            let backend = backend.clone();
7242            async move {
7243                futures::future::try_join3(
7244                    backend.branches(),
7245                    head_commit_future,
7246                    backend.worktrees(),
7247                )
7248                .await
7249            }
7250        })
7251        .await?;
7252    let branch = branches.into_iter().find(|branch| branch.is_head);
7253
7254    let linked_worktrees: Arc<[GitWorktree]> = all_worktrees
7255        .into_iter()
7256        .filter(|wt| wt.path != *work_directory_abs_path)
7257        .collect();
7258
7259    let (remote_origin_url, remote_upstream_url) = cx
7260        .background_spawn({
7261            let backend = backend.clone();
7262            async move {
7263                Ok::<_, anyhow::Error>(
7264                    futures::future::join(
7265                        backend.remote_url("origin"),
7266                        backend.remote_url("upstream"),
7267                    )
7268                    .await,
7269                )
7270            }
7271        })
7272        .await?;
7273
7274    let snapshot = this.update(cx, |this, cx| {
7275        let branch_changed =
7276            branch != this.snapshot.branch || head_commit != this.snapshot.head_commit;
7277        let worktrees_changed = *linked_worktrees != *this.snapshot.linked_worktrees;
7278
7279        this.snapshot = RepositorySnapshot {
7280            id,
7281            work_directory_abs_path,
7282            branch,
7283            head_commit,
7284            remote_origin_url,
7285            remote_upstream_url,
7286            linked_worktrees,
7287            scan_id: prev_snapshot.scan_id + 1,
7288            ..prev_snapshot
7289        };
7290
7291        if branch_changed {
7292            cx.emit(RepositoryEvent::BranchChanged);
7293        }
7294
7295        if worktrees_changed {
7296            cx.emit(RepositoryEvent::GitWorktreeListChanged);
7297        }
7298
7299        this.snapshot.clone()
7300    });
7301
7302    let (statuses, diff_stats, stash_entries) = cx
7303        .background_spawn({
7304            let backend = backend.clone();
7305            let snapshot = snapshot.clone();
7306            async move {
7307                let diff_stat_future: BoxFuture<'_, Result<status::GitDiffStat>> =
7308                    if snapshot.head_commit.is_some() {
7309                        backend.diff_stat(&[])
7310                    } else {
7311                        future::ready(Ok(status::GitDiffStat {
7312                            entries: Arc::default(),
7313                        }))
7314                        .boxed()
7315                    };
7316                futures::future::try_join3(
7317                    backend.status(&[RepoPath::from_rel_path(
7318                        &RelPath::new(".".as_ref(), PathStyle::local()).unwrap(),
7319                    )]),
7320                    diff_stat_future,
7321                    backend.stash_entries(),
7322                )
7323                .await
7324            }
7325        })
7326        .await?;
7327
7328    let diff_stat_map: HashMap<&RepoPath, DiffStat> =
7329        diff_stats.entries.iter().map(|(p, s)| (p, *s)).collect();
7330    let mut conflicted_paths = Vec::new();
7331    let statuses_by_path = SumTree::from_iter(
7332        statuses.entries.iter().map(|(repo_path, status)| {
7333            if status.is_conflicted() {
7334                conflicted_paths.push(repo_path.clone());
7335            }
7336            StatusEntry {
7337                repo_path: repo_path.clone(),
7338                status: *status,
7339                diff_stat: diff_stat_map.get(repo_path).copied(),
7340            }
7341        }),
7342        (),
7343    );
7344
7345    let merge_details = cx
7346        .background_spawn({
7347            let backend = backend.clone();
7348            let mut merge_details = snapshot.merge.clone();
7349            async move {
7350                let conflicts_changed = merge_details.update(&backend, conflicted_paths).await?;
7351                Ok::<_, anyhow::Error>((merge_details, conflicts_changed))
7352            }
7353        })
7354        .await?;
7355    let (merge_details, conflicts_changed) = merge_details;
7356    log::debug!("new merge details: {merge_details:?}");
7357
7358    Ok(this.update(cx, |this, cx| {
7359        if conflicts_changed || statuses_by_path != this.snapshot.statuses_by_path {
7360            cx.emit(RepositoryEvent::StatusesChanged);
7361        }
7362        if stash_entries != this.snapshot.stash_entries {
7363            cx.emit(RepositoryEvent::StashEntriesChanged);
7364        }
7365
7366        this.snapshot.scan_id += 1;
7367        this.snapshot.merge = merge_details;
7368        this.snapshot.statuses_by_path = statuses_by_path;
7369        this.snapshot.stash_entries = stash_entries;
7370
7371        this.snapshot.clone()
7372    }))
7373}
7374
7375fn status_from_proto(
7376    simple_status: i32,
7377    status: Option<proto::GitFileStatus>,
7378) -> anyhow::Result<FileStatus> {
7379    use proto::git_file_status::Variant;
7380
7381    let Some(variant) = status.and_then(|status| status.variant) else {
7382        let code = proto::GitStatus::from_i32(simple_status)
7383            .with_context(|| format!("Invalid git status code: {simple_status}"))?;
7384        let result = match code {
7385            proto::GitStatus::Added => TrackedStatus {
7386                worktree_status: StatusCode::Added,
7387                index_status: StatusCode::Unmodified,
7388            }
7389            .into(),
7390            proto::GitStatus::Modified => TrackedStatus {
7391                worktree_status: StatusCode::Modified,
7392                index_status: StatusCode::Unmodified,
7393            }
7394            .into(),
7395            proto::GitStatus::Conflict => UnmergedStatus {
7396                first_head: UnmergedStatusCode::Updated,
7397                second_head: UnmergedStatusCode::Updated,
7398            }
7399            .into(),
7400            proto::GitStatus::Deleted => TrackedStatus {
7401                worktree_status: StatusCode::Deleted,
7402                index_status: StatusCode::Unmodified,
7403            }
7404            .into(),
7405            _ => anyhow::bail!("Invalid code for simple status: {simple_status}"),
7406        };
7407        return Ok(result);
7408    };
7409
7410    let result = match variant {
7411        Variant::Untracked(_) => FileStatus::Untracked,
7412        Variant::Ignored(_) => FileStatus::Ignored,
7413        Variant::Unmerged(unmerged) => {
7414            let [first_head, second_head] =
7415                [unmerged.first_head, unmerged.second_head].map(|head| {
7416                    let code = proto::GitStatus::from_i32(head)
7417                        .with_context(|| format!("Invalid git status code: {head}"))?;
7418                    let result = match code {
7419                        proto::GitStatus::Added => UnmergedStatusCode::Added,
7420                        proto::GitStatus::Updated => UnmergedStatusCode::Updated,
7421                        proto::GitStatus::Deleted => UnmergedStatusCode::Deleted,
7422                        _ => anyhow::bail!("Invalid code for unmerged status: {code:?}"),
7423                    };
7424                    Ok(result)
7425                });
7426            let [first_head, second_head] = [first_head?, second_head?];
7427            UnmergedStatus {
7428                first_head,
7429                second_head,
7430            }
7431            .into()
7432        }
7433        Variant::Tracked(tracked) => {
7434            let [index_status, worktree_status] = [tracked.index_status, tracked.worktree_status]
7435                .map(|status| {
7436                    let code = proto::GitStatus::from_i32(status)
7437                        .with_context(|| format!("Invalid git status code: {status}"))?;
7438                    let result = match code {
7439                        proto::GitStatus::Modified => StatusCode::Modified,
7440                        proto::GitStatus::TypeChanged => StatusCode::TypeChanged,
7441                        proto::GitStatus::Added => StatusCode::Added,
7442                        proto::GitStatus::Deleted => StatusCode::Deleted,
7443                        proto::GitStatus::Renamed => StatusCode::Renamed,
7444                        proto::GitStatus::Copied => StatusCode::Copied,
7445                        proto::GitStatus::Unmodified => StatusCode::Unmodified,
7446                        _ => anyhow::bail!("Invalid code for tracked status: {code:?}"),
7447                    };
7448                    Ok(result)
7449                });
7450            let [index_status, worktree_status] = [index_status?, worktree_status?];
7451            TrackedStatus {
7452                index_status,
7453                worktree_status,
7454            }
7455            .into()
7456        }
7457    };
7458    Ok(result)
7459}
7460
7461fn status_to_proto(status: FileStatus) -> proto::GitFileStatus {
7462    use proto::git_file_status::{Tracked, Unmerged, Variant};
7463
7464    let variant = match status {
7465        FileStatus::Untracked => Variant::Untracked(Default::default()),
7466        FileStatus::Ignored => Variant::Ignored(Default::default()),
7467        FileStatus::Unmerged(UnmergedStatus {
7468            first_head,
7469            second_head,
7470        }) => Variant::Unmerged(Unmerged {
7471            first_head: unmerged_status_to_proto(first_head),
7472            second_head: unmerged_status_to_proto(second_head),
7473        }),
7474        FileStatus::Tracked(TrackedStatus {
7475            index_status,
7476            worktree_status,
7477        }) => Variant::Tracked(Tracked {
7478            index_status: tracked_status_to_proto(index_status),
7479            worktree_status: tracked_status_to_proto(worktree_status),
7480        }),
7481    };
7482    proto::GitFileStatus {
7483        variant: Some(variant),
7484    }
7485}
7486
7487fn unmerged_status_to_proto(code: UnmergedStatusCode) -> i32 {
7488    match code {
7489        UnmergedStatusCode::Added => proto::GitStatus::Added as _,
7490        UnmergedStatusCode::Deleted => proto::GitStatus::Deleted as _,
7491        UnmergedStatusCode::Updated => proto::GitStatus::Updated as _,
7492    }
7493}
7494
7495fn tracked_status_to_proto(code: StatusCode) -> i32 {
7496    match code {
7497        StatusCode::Added => proto::GitStatus::Added as _,
7498        StatusCode::Deleted => proto::GitStatus::Deleted as _,
7499        StatusCode::Modified => proto::GitStatus::Modified as _,
7500        StatusCode::Renamed => proto::GitStatus::Renamed as _,
7501        StatusCode::TypeChanged => proto::GitStatus::TypeChanged as _,
7502        StatusCode::Copied => proto::GitStatus::Copied as _,
7503        StatusCode::Unmodified => proto::GitStatus::Unmodified as _,
7504    }
7505}