1773 lines · 64.8 KB · Rust
RawDownload
use crate::{    buffer_store::{BufferStore, BufferStoreEvent},    worktree_store::{WorktreeStore, WorktreeStoreEvent},    Project, ProjectItem, ProjectPath,};use anyhow::{Context as _, Result};use askpass::{AskPassDelegate, AskPassSession};use buffer_diff::BufferDiffEvent;use client::ProjectId;use collections::HashMap;use futures::{    channel::{mpsc, oneshot},    StreamExt as _,};use git::repository::DiffType;use git::{    repository::{        Branch, CommitDetails, GitRepository, PushOptions, Remote, RemoteCommandOutput, RepoPath,        ResetMode,    },    status::FileStatus,};use gpui::{    App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,    WeakEntity,};use language::{Buffer, LanguageRegistry};use parking_lot::Mutex;use rpc::{    proto::{self, git_reset, ToProto},    AnyProtoClient, TypedEnvelope,};use settings::WorktreeId;use std::{    collections::VecDeque,    future::Future,    path::{Path, PathBuf},    sync::Arc,};use text::BufferId;use util::{debug_panic, maybe, ResultExt};use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory}; pub struct GitStore {    buffer_store: Entity<BufferStore>,    pub(super) project_id: Option<ProjectId>,    pub(super) client: AnyProtoClient,    repositories: Vec<Entity<Repository>>,    active_index: Option<usize>,    update_sender: mpsc::UnboundedSender<GitJob>,    _subscriptions: [Subscription; 2],} pub struct Repository {    commit_message_buffer: Option<Entity<Buffer>>,    git_store: WeakEntity<GitStore>,    pub worktree_id: WorktreeId,    pub repository_entry: RepositoryEntry,    pub dot_git_abs_path: PathBuf,    pub worktree_abs_path: Arc<Path>,    pub is_from_single_file_worktree: bool,    pub git_repo: GitRepo,    pub merge_message: Option<String>,    job_sender: mpsc::UnboundedSender<GitJob>,    askpass_delegates: Arc<Mutex<HashMap<u64, AskPassDelegate>>>,    latest_askpass_id: u64,} #[derive(Clone)]pub enum GitRepo {    Local(Arc<dyn GitRepository>),    Remote {        project_id: ProjectId,        client: AnyProtoClient,        worktree_id: WorktreeId,        work_directory_id: ProjectEntryId,    },} #[derive(Debug)]pub enum GitEvent {    ActiveRepositoryChanged,    FileSystemUpdated,    GitStateUpdated,    IndexWriteError(anyhow::Error),} struct GitJob {    job: Box<dyn FnOnce(&mut AsyncApp) -> Task<()>>,    key: Option<GitJobKey>,} #[derive(PartialEq, Eq)]enum GitJobKey {    WriteIndex(RepoPath),} impl EventEmitter<GitEvent> for GitStore {} impl GitStore {    pub fn new(        worktree_store: &Entity<WorktreeStore>,        buffer_store: Entity<BufferStore>,        client: AnyProtoClient,        project_id: Option<ProjectId>,        cx: &mut Context<'_, Self>,    ) -> Self {        let update_sender = Self::spawn_git_worker(cx);        let _subscriptions = [            cx.subscribe(worktree_store, Self::on_worktree_store_event),            cx.subscribe(&buffer_store, Self::on_buffer_store_event),        ];         GitStore {            project_id,            client,            buffer_store,            repositories: Vec::new(),            active_index: None,            update_sender,            _subscriptions,        }    }     pub fn init(client: &AnyProtoClient) {        client.add_entity_request_handler(Self::handle_get_remotes);        client.add_entity_request_handler(Self::handle_get_branches);        client.add_entity_request_handler(Self::handle_change_branch);        client.add_entity_request_handler(Self::handle_create_branch);        client.add_entity_request_handler(Self::handle_push);        client.add_entity_request_handler(Self::handle_pull);        client.add_entity_request_handler(Self::handle_fetch);        client.add_entity_request_handler(Self::handle_stage);        client.add_entity_request_handler(Self::handle_unstage);        client.add_entity_request_handler(Self::handle_commit);        client.add_entity_request_handler(Self::handle_reset);        client.add_entity_request_handler(Self::handle_show);        client.add_entity_request_handler(Self::handle_checkout_files);        client.add_entity_request_handler(Self::handle_open_commit_message_buffer);        client.add_entity_request_handler(Self::handle_set_index_text);        client.add_entity_request_handler(Self::handle_askpass);        client.add_entity_request_handler(Self::handle_check_for_pushed_commits);        client.add_entity_request_handler(Self::handle_git_diff);    }     pub fn active_repository(&self) -> Option<Entity<Repository>> {        self.active_index            .map(|index| self.repositories[index].clone())    }     fn on_worktree_store_event(        &mut self,        worktree_store: Entity<WorktreeStore>,        event: &WorktreeStoreEvent,        cx: &mut Context<'_, Self>,    ) {        let mut new_repositories = Vec::new();        let mut new_active_index = None;        let this = cx.weak_entity();        let client = self.client.clone();        let project_id = self.project_id;         worktree_store.update(cx, |worktree_store, cx| {            for worktree in worktree_store.worktrees() {                worktree.update(cx, |worktree, cx| {                    let snapshot = worktree.snapshot();                    for repo in snapshot.repositories().iter() {                        let git_data = worktree                            .as_local()                            .and_then(|local_worktree| local_worktree.get_local_repo(repo))                            .map(|local_repo| {                                (                                    GitRepo::Local(local_repo.repo().clone()),                                    local_repo.merge_message.clone(),                                )                            })                            .or_else(|| {                                let client = client.clone();                                let project_id = project_id?;                                Some((                                    GitRepo::Remote {                                        project_id,                                        client,                                        worktree_id: worktree.id(),                                        work_directory_id: repo.work_directory_id(),                                    },                                    None,                                ))                            });                        let Some((git_repo, merge_message)) = git_data else {                            continue;                        };                        let worktree_id = worktree.id();                        let existing =                            self.repositories                                .iter()                                .enumerate()                                .find(|(_, existing_handle)| {                                    existing_handle.read(cx).id()                                        == (worktree_id, repo.work_directory_id())                                });                        let handle = if let Some((index, handle)) = existing {                            if self.active_index == Some(index) {                                new_active_index = Some(new_repositories.len());                            }                            // Update the statuses and merge message but keep everything else.                            let existing_handle = handle.clone();                            existing_handle.update(cx, |existing_handle, cx| {                                existing_handle.repository_entry = repo.clone();                                if matches!(git_repo, GitRepo::Local { .. })                                    && existing_handle.merge_message != merge_message                                {                                    if let (Some(merge_message), Some(buffer)) =                                        (&merge_message, &existing_handle.commit_message_buffer)                                    {                                        buffer.update(cx, |buffer, cx| {                                            if buffer.is_empty() {                                                buffer.set_text(merge_message.as_str(), cx);                                            }                                        })                                    }                                    existing_handle.merge_message = merge_message;                                }                            });                            existing_handle                        } else {                            cx.new(|_| Repository {                                git_store: this.clone(),                                worktree_id,                                askpass_delegates: Default::default(),                                latest_askpass_id: 0,                                repository_entry: repo.clone(),                                dot_git_abs_path: worktree.dot_git_abs_path(&repo.work_directory),                                worktree_abs_path: worktree.abs_path(),                                is_from_single_file_worktree: worktree.is_single_file(),                                git_repo,                                job_sender: self.update_sender.clone(),                                merge_message,                                commit_message_buffer: None,                            })                        };                        new_repositories.push(handle);                    }                })            }        });         if new_active_index == None && new_repositories.len() > 0 {            new_active_index = Some(0);        }         self.repositories = new_repositories;        self.active_index = new_active_index;         match event {            WorktreeStoreEvent::WorktreeUpdatedGitRepositories(_) => {                cx.emit(GitEvent::GitStateUpdated);            }            _ => {                cx.emit(GitEvent::FileSystemUpdated);            }        }    }     fn on_buffer_store_event(        &mut self,        _: Entity<BufferStore>,        event: &BufferStoreEvent,        cx: &mut Context<'_, Self>,    ) {        if let BufferStoreEvent::BufferDiffAdded(diff) = event {            cx.subscribe(diff, Self::on_buffer_diff_event).detach();        }    }     fn on_buffer_diff_event(        this: &mut GitStore,        diff: Entity<buffer_diff::BufferDiff>,        event: &BufferDiffEvent,        cx: &mut Context<'_, GitStore>,    ) {        if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {            let buffer_id = diff.read(cx).buffer_id;            if let Some((repo, path)) = this.repository_and_path_for_buffer_id(buffer_id, cx) {                let recv = repo                    .read(cx)                    .set_index_text(&path, new_index_text.as_ref().map(|rope| rope.to_string()));                let diff = diff.downgrade();                cx.spawn(|this, mut cx| async move {                    if let Some(result) = cx.background_spawn(async move { recv.await.ok() }).await                    {                        if let Err(error) = result {                            diff.update(&mut cx, |diff, cx| {                                diff.clear_pending_hunks(cx);                            })                            .ok();                            this.update(&mut cx, |_, cx| cx.emit(GitEvent::IndexWriteError(error)))                                .ok();                        }                    }                })                .detach();            }        }    }     pub fn all_repositories(&self) -> Vec<Entity<Repository>> {        self.repositories.clone()    }     pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {        let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?;        let status = repo.read(cx).repository_entry.status_for_path(&path)?;        Some(status.status)    }     fn repository_and_path_for_buffer_id(        &self,        buffer_id: BufferId,        cx: &App,    ) -> Option<(Entity<Repository>, RepoPath)> {        let buffer = self.buffer_store.read(cx).get(buffer_id)?;        let path = buffer.read(cx).project_path(cx)?;        let mut result: Option<(Entity<Repository>, RepoPath)> = None;        for repo_handle in &self.repositories {            let repo = repo_handle.read(cx);            if repo.worktree_id == path.worktree_id {                if let Ok(relative_path) = repo.repository_entry.relativize(&path.path) {                    if result                        .as_ref()                        .is_none_or(|(result, _)| !repo.contains_sub_repo(result, cx))                    {                        result = Some((repo_handle.clone(), relative_path))                    }                }            }        }        result    }     fn spawn_git_worker(cx: &mut Context<'_, GitStore>) -> mpsc::UnboundedSender<GitJob> {        let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();         cx.spawn(|_, mut cx| async move {            let mut jobs = VecDeque::new();            loop {                while let Ok(Some(next_job)) = job_rx.try_next() {                    jobs.push_back(next_job);                }                 if let Some(job) = jobs.pop_front() {                    if let Some(current_key) = &job.key {                        if jobs                            .iter()                            .any(|other_job| other_job.key.as_ref() == Some(current_key))                        {                            continue;                        }                    }                    (job.job)(&mut cx).await;                } else if let Some(job) = job_rx.next().await {                    jobs.push_back(job);                } else {                    break;                }            }        })        .detach();        job_tx    }     async fn handle_fetch(        this: Entity<Self>,        envelope: TypedEnvelope<proto::Fetch>,        mut cx: AsyncApp,    ) -> Result<proto::RemoteMessageResponse> {        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);        let repository_handle =            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;        let askpass_id = envelope.payload.askpass_id;         let askpass = make_remote_delegate(            this,            envelope.payload.project_id,            worktree_id,            work_directory_id,            askpass_id,            &mut cx,        );         let remote_output = repository_handle            .update(&mut cx, |repository_handle, cx| {                repository_handle.fetch(askpass, cx)            })?            .await??;         Ok(proto::RemoteMessageResponse {            stdout: remote_output.stdout,            stderr: remote_output.stderr,        })    }     async fn handle_push(        this: Entity<Self>,        envelope: TypedEnvelope<proto::Push>,        mut cx: AsyncApp,    ) -> Result<proto::RemoteMessageResponse> {        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);        let repository_handle =            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;         let askpass_id = envelope.payload.askpass_id;        let askpass = make_remote_delegate(            this,            envelope.payload.project_id,            worktree_id,            work_directory_id,            askpass_id,            &mut cx,        );         let options = envelope            .payload            .options            .as_ref()            .map(|_| match envelope.payload.options() {                proto::push::PushOptions::SetUpstream => git::repository::PushOptions::SetUpstream,                proto::push::PushOptions::Force => git::repository::PushOptions::Force,            });         let branch_name = envelope.payload.branch_name.into();        let remote_name = envelope.payload.remote_name.into();         let remote_output = repository_handle            .update(&mut cx, |repository_handle, cx| {                repository_handle.push(branch_name, remote_name, options, askpass, cx)            })?            .await??;        Ok(proto::RemoteMessageResponse {            stdout: remote_output.stdout,            stderr: remote_output.stderr,        })    }     async fn handle_pull(        this: Entity<Self>,        envelope: TypedEnvelope<proto::Pull>,        mut cx: AsyncApp,    ) -> Result<proto::RemoteMessageResponse> {        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);        let repository_handle =            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;        let askpass_id = envelope.payload.askpass_id;        let askpass = make_remote_delegate(            this,            envelope.payload.project_id,            worktree_id,            work_directory_id,            askpass_id,            &mut cx,        );         let branch_name = envelope.payload.branch_name.into();        let remote_name = envelope.payload.remote_name.into();         let remote_message = repository_handle            .update(&mut cx, |repository_handle, cx| {                repository_handle.pull(branch_name, remote_name, askpass, cx)            })?            .await??;         Ok(proto::RemoteMessageResponse {            stdout: remote_message.stdout,            stderr: remote_message.stderr,        })    }     async fn handle_stage(        this: Entity<Self>,        envelope: TypedEnvelope<proto::Stage>,        mut cx: AsyncApp,    ) -> Result<proto::Ack> {        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);        let repository_handle =            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;         let entries = envelope            .payload            .paths            .into_iter()            .map(PathBuf::from)            .map(RepoPath::new)            .collect();         repository_handle            .update(&mut cx, |repository_handle, cx| {

Lines 1–500 of 1773.