git.rs

   1use crate::buffer_store::BufferStore;
   2use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
   3use crate::{Project, ProjectPath};
   4use anyhow::{Context as _, Result};
   5use client::ProjectId;
   6use futures::channel::{mpsc, oneshot};
   7use futures::StreamExt as _;
   8use git::repository::{Branch, CommitDetails, ResetMode};
   9use git::{
  10    repository::{GitRepository, RepoPath},
  11    status::{GitSummary, TrackedSummary},
  12};
  13use gpui::{
  14    App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription,
  15    Task, WeakEntity,
  16};
  17use language::{Buffer, LanguageRegistry};
  18use rpc::proto::{git_reset, ToProto};
  19use rpc::{proto, AnyProtoClient, TypedEnvelope};
  20use settings::WorktreeId;
  21use std::path::{Path, PathBuf};
  22use std::sync::Arc;
  23use text::BufferId;
  24use util::{maybe, ResultExt};
  25use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
  26
  27pub struct GitStore {
  28    buffer_store: Entity<BufferStore>,
  29    pub(super) project_id: Option<ProjectId>,
  30    pub(super) client: Option<AnyProtoClient>,
  31    repositories: Vec<Entity<Repository>>,
  32    active_index: Option<usize>,
  33    update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)>,
  34    _subscription: Subscription,
  35}
  36
  37pub struct Repository {
  38    commit_message_buffer: Option<Entity<Buffer>>,
  39    git_store: WeakEntity<GitStore>,
  40    pub worktree_id: WorktreeId,
  41    pub repository_entry: RepositoryEntry,
  42    pub git_repo: GitRepo,
  43    pub merge_message: Option<String>,
  44    update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)>,
  45}
  46
  47#[derive(Clone)]
  48pub enum GitRepo {
  49    Local(Arc<dyn GitRepository>),
  50    Remote {
  51        project_id: ProjectId,
  52        client: AnyProtoClient,
  53        worktree_id: WorktreeId,
  54        work_directory_id: ProjectEntryId,
  55    },
  56}
  57
  58pub enum Message {
  59    Commit {
  60        git_repo: GitRepo,
  61        message: SharedString,
  62        name_and_email: Option<(SharedString, SharedString)>,
  63    },
  64    Reset {
  65        repo: GitRepo,
  66        commit: SharedString,
  67        reset_mode: ResetMode,
  68    },
  69    CheckoutFiles {
  70        repo: GitRepo,
  71        commit: SharedString,
  72        paths: Vec<RepoPath>,
  73    },
  74    Stage(GitRepo, Vec<RepoPath>),
  75    Unstage(GitRepo, Vec<RepoPath>),
  76    SetIndexText(GitRepo, RepoPath, Option<String>),
  77}
  78
  79pub enum GitEvent {
  80    ActiveRepositoryChanged,
  81    FileSystemUpdated,
  82    GitStateUpdated,
  83}
  84
  85impl EventEmitter<GitEvent> for GitStore {}
  86
  87impl GitStore {
  88    pub fn new(
  89        worktree_store: &Entity<WorktreeStore>,
  90        buffer_store: Entity<BufferStore>,
  91        client: Option<AnyProtoClient>,
  92        project_id: Option<ProjectId>,
  93        cx: &mut Context<'_, Self>,
  94    ) -> Self {
  95        let update_sender = Self::spawn_git_worker(cx);
  96        let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
  97
  98        GitStore {
  99            project_id,
 100            client,
 101            buffer_store,
 102            repositories: Vec::new(),
 103            active_index: None,
 104            update_sender,
 105            _subscription,
 106        }
 107    }
 108
 109    pub fn init(client: &AnyProtoClient) {
 110        client.add_entity_request_handler(Self::handle_stage);
 111        client.add_entity_request_handler(Self::handle_unstage);
 112        client.add_entity_request_handler(Self::handle_commit);
 113        client.add_entity_request_handler(Self::handle_reset);
 114        client.add_entity_request_handler(Self::handle_show);
 115        client.add_entity_request_handler(Self::handle_checkout_files);
 116        client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
 117        client.add_entity_request_handler(Self::handle_set_index_text);
 118    }
 119
 120    pub fn active_repository(&self) -> Option<Entity<Repository>> {
 121        self.active_index
 122            .map(|index| self.repositories[index].clone())
 123    }
 124
 125    fn on_worktree_store_event(
 126        &mut self,
 127        worktree_store: Entity<WorktreeStore>,
 128        event: &WorktreeStoreEvent,
 129        cx: &mut Context<'_, Self>,
 130    ) {
 131        let mut new_repositories = Vec::new();
 132        let mut new_active_index = None;
 133        let this = cx.weak_entity();
 134        let client = self.client.clone();
 135        let project_id = self.project_id;
 136
 137        worktree_store.update(cx, |worktree_store, cx| {
 138            for worktree in worktree_store.worktrees() {
 139                worktree.update(cx, |worktree, cx| {
 140                    let snapshot = worktree.snapshot();
 141                    for repo in snapshot.repositories().iter() {
 142                        let git_data = worktree
 143                            .as_local()
 144                            .and_then(|local_worktree| local_worktree.get_local_repo(repo))
 145                            .map(|local_repo| {
 146                                (
 147                                    GitRepo::Local(local_repo.repo().clone()),
 148                                    local_repo.merge_message.clone(),
 149                                )
 150                            })
 151                            .or_else(|| {
 152                                let client = client.clone()?;
 153                                let project_id = project_id?;
 154                                Some((
 155                                    GitRepo::Remote {
 156                                        project_id,
 157                                        client,
 158                                        worktree_id: worktree.id(),
 159                                        work_directory_id: repo.work_directory_id(),
 160                                    },
 161                                    None,
 162                                ))
 163                            });
 164                        let Some((git_repo, merge_message)) = git_data else {
 165                            continue;
 166                        };
 167                        let worktree_id = worktree.id();
 168                        let existing =
 169                            self.repositories
 170                                .iter()
 171                                .enumerate()
 172                                .find(|(_, existing_handle)| {
 173                                    existing_handle.read(cx).id()
 174                                        == (worktree_id, repo.work_directory_id())
 175                                });
 176                        let handle = if let Some((index, handle)) = existing {
 177                            if self.active_index == Some(index) {
 178                                new_active_index = Some(new_repositories.len());
 179                            }
 180                            // Update the statuses and merge message but keep everything else.
 181                            let existing_handle = handle.clone();
 182                            existing_handle.update(cx, |existing_handle, cx| {
 183                                existing_handle.repository_entry = repo.clone();
 184                                if matches!(git_repo, GitRepo::Local { .. })
 185                                    && existing_handle.merge_message != merge_message
 186                                {
 187                                    if let (Some(merge_message), Some(buffer)) =
 188                                        (&merge_message, &existing_handle.commit_message_buffer)
 189                                    {
 190                                        buffer.update(cx, |buffer, cx| {
 191                                            if buffer.is_empty() {
 192                                                buffer.set_text(merge_message.as_str(), cx);
 193                                            }
 194                                        })
 195                                    }
 196                                    existing_handle.merge_message = merge_message;
 197                                }
 198                            });
 199                            existing_handle
 200                        } else {
 201                            cx.new(|_| Repository {
 202                                git_store: this.clone(),
 203                                worktree_id,
 204                                repository_entry: repo.clone(),
 205                                git_repo,
 206                                update_sender: self.update_sender.clone(),
 207                                merge_message,
 208                                commit_message_buffer: None,
 209                            })
 210                        };
 211                        new_repositories.push(handle);
 212                    }
 213                })
 214            }
 215        });
 216
 217        if new_active_index == None && new_repositories.len() > 0 {
 218            new_active_index = Some(0);
 219        }
 220
 221        self.repositories = new_repositories;
 222        self.active_index = new_active_index;
 223
 224        match event {
 225            WorktreeStoreEvent::WorktreeUpdatedGitRepositories(_) => {
 226                cx.emit(GitEvent::GitStateUpdated);
 227            }
 228            _ => {
 229                cx.emit(GitEvent::FileSystemUpdated);
 230            }
 231        }
 232    }
 233
 234    pub fn all_repositories(&self) -> Vec<Entity<Repository>> {
 235        self.repositories.clone()
 236    }
 237
 238    fn spawn_git_worker(
 239        cx: &mut Context<'_, GitStore>,
 240    ) -> mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)> {
 241        let (update_sender, mut update_receiver) =
 242            mpsc::unbounded::<(Message, oneshot::Sender<Result<()>>)>();
 243        cx.spawn(|_, cx| async move {
 244            while let Some((msg, respond)) = update_receiver.next().await {
 245                let result = cx.background_spawn(Self::process_git_msg(msg)).await;
 246                respond.send(result).ok();
 247            }
 248        })
 249        .detach();
 250        update_sender
 251    }
 252
 253    async fn process_git_msg(msg: Message) -> Result<()> {
 254        match msg {
 255            Message::Stage(repo, paths) => {
 256                match repo {
 257                    GitRepo::Local(repo) => repo.stage_paths(&paths)?,
 258                    GitRepo::Remote {
 259                        project_id,
 260                        client,
 261                        worktree_id,
 262                        work_directory_id,
 263                    } => {
 264                        client
 265                            .request(proto::Stage {
 266                                project_id: project_id.0,
 267                                worktree_id: worktree_id.to_proto(),
 268                                work_directory_id: work_directory_id.to_proto(),
 269                                paths: paths
 270                                    .into_iter()
 271                                    .map(|repo_path| repo_path.as_ref().to_proto())
 272                                    .collect(),
 273                            })
 274                            .await
 275                            .context("sending stage request")?;
 276                    }
 277                }
 278                Ok(())
 279            }
 280            Message::Reset {
 281                repo,
 282                commit,
 283                reset_mode,
 284            } => {
 285                match repo {
 286                    GitRepo::Local(repo) => repo.reset(&commit, reset_mode)?,
 287                    GitRepo::Remote {
 288                        project_id,
 289                        client,
 290                        worktree_id,
 291                        work_directory_id,
 292                    } => {
 293                        client
 294                            .request(proto::GitReset {
 295                                project_id: project_id.0,
 296                                worktree_id: worktree_id.to_proto(),
 297                                work_directory_id: work_directory_id.to_proto(),
 298                                commit: commit.into(),
 299                                mode: match reset_mode {
 300                                    ResetMode::Soft => git_reset::ResetMode::Soft.into(),
 301                                    ResetMode::Mixed => git_reset::ResetMode::Mixed.into(),
 302                                },
 303                            })
 304                            .await?;
 305                    }
 306                }
 307                Ok(())
 308            }
 309
 310            Message::CheckoutFiles {
 311                repo,
 312                commit,
 313                paths,
 314            } => {
 315                match repo {
 316                    GitRepo::Local(repo) => repo.checkout_files(&commit, &paths)?,
 317                    GitRepo::Remote {
 318                        project_id,
 319                        client,
 320                        worktree_id,
 321                        work_directory_id,
 322                    } => {
 323                        client
 324                            .request(proto::GitCheckoutFiles {
 325                                project_id: project_id.0,
 326                                worktree_id: worktree_id.to_proto(),
 327                                work_directory_id: work_directory_id.to_proto(),
 328                                commit: commit.into(),
 329                                paths: paths
 330                                    .into_iter()
 331                                    .map(|p| p.to_string_lossy().to_string())
 332                                    .collect(),
 333                            })
 334                            .await?;
 335                    }
 336                }
 337                Ok(())
 338            }
 339            Message::Unstage(repo, paths) => {
 340                match repo {
 341                    GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
 342                    GitRepo::Remote {
 343                        project_id,
 344                        client,
 345                        worktree_id,
 346                        work_directory_id,
 347                    } => {
 348                        client
 349                            .request(proto::Unstage {
 350                                project_id: project_id.0,
 351                                worktree_id: worktree_id.to_proto(),
 352                                work_directory_id: work_directory_id.to_proto(),
 353                                paths: paths
 354                                    .into_iter()
 355                                    .map(|repo_path| repo_path.as_ref().to_proto())
 356                                    .collect(),
 357                            })
 358                            .await
 359                            .context("sending unstage request")?;
 360                    }
 361                }
 362                Ok(())
 363            }
 364            Message::Commit {
 365                git_repo,
 366                message,
 367                name_and_email,
 368            } => {
 369                match git_repo {
 370                    GitRepo::Local(repo) => repo.commit(
 371                        message.as_ref(),
 372                        name_and_email
 373                            .as_ref()
 374                            .map(|(name, email)| (name.as_ref(), email.as_ref())),
 375                    )?,
 376                    GitRepo::Remote {
 377                        project_id,
 378                        client,
 379                        worktree_id,
 380                        work_directory_id,
 381                    } => {
 382                        let (name, email) = name_and_email.unzip();
 383                        client
 384                            .request(proto::Commit {
 385                                project_id: project_id.0,
 386                                worktree_id: worktree_id.to_proto(),
 387                                work_directory_id: work_directory_id.to_proto(),
 388                                message: String::from(message),
 389                                name: name.map(String::from),
 390                                email: email.map(String::from),
 391                            })
 392                            .await
 393                            .context("sending commit request")?;
 394                    }
 395                }
 396                Ok(())
 397            }
 398            Message::SetIndexText(git_repo, path, text) => match git_repo {
 399                GitRepo::Local(repo) => repo.set_index_text(&path, text),
 400                GitRepo::Remote {
 401                    project_id,
 402                    client,
 403                    worktree_id,
 404                    work_directory_id,
 405                } => client.send(proto::SetIndexText {
 406                    project_id: project_id.0,
 407                    worktree_id: worktree_id.to_proto(),
 408                    work_directory_id: work_directory_id.to_proto(),
 409                    path: path.as_ref().to_proto(),
 410                    text,
 411                }),
 412            },
 413        }
 414    }
 415
 416    async fn handle_stage(
 417        this: Entity<Self>,
 418        envelope: TypedEnvelope<proto::Stage>,
 419        mut cx: AsyncApp,
 420    ) -> Result<proto::Ack> {
 421        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 422        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 423        let repository_handle =
 424            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 425
 426        let entries = envelope
 427            .payload
 428            .paths
 429            .into_iter()
 430            .map(PathBuf::from)
 431            .map(RepoPath::new)
 432            .collect();
 433
 434        repository_handle
 435            .update(&mut cx, |repository_handle, cx| {
 436                repository_handle.stage_entries(entries, cx)
 437            })?
 438            .await?;
 439        Ok(proto::Ack {})
 440    }
 441
 442    async fn handle_unstage(
 443        this: Entity<Self>,
 444        envelope: TypedEnvelope<proto::Unstage>,
 445        mut cx: AsyncApp,
 446    ) -> Result<proto::Ack> {
 447        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 448        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 449        let repository_handle =
 450            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 451
 452        let entries = envelope
 453            .payload
 454            .paths
 455            .into_iter()
 456            .map(PathBuf::from)
 457            .map(RepoPath::new)
 458            .collect();
 459
 460        repository_handle
 461            .update(&mut cx, |repository_handle, cx| {
 462                repository_handle.unstage_entries(entries, cx)
 463            })?
 464            .await?;
 465
 466        Ok(proto::Ack {})
 467    }
 468
 469    async fn handle_set_index_text(
 470        this: Entity<Self>,
 471        envelope: TypedEnvelope<proto::SetIndexText>,
 472        mut cx: AsyncApp,
 473    ) -> Result<proto::Ack> {
 474        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 475        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 476        let repository_handle =
 477            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 478
 479        repository_handle
 480            .update(&mut cx, |repository_handle, _| {
 481                repository_handle.set_index_text(
 482                    &RepoPath::from_str(&envelope.payload.path),
 483                    envelope.payload.text,
 484                )
 485            })?
 486            .await??;
 487        Ok(proto::Ack {})
 488    }
 489
 490    async fn handle_commit(
 491        this: Entity<Self>,
 492        envelope: TypedEnvelope<proto::Commit>,
 493        mut cx: AsyncApp,
 494    ) -> Result<proto::Ack> {
 495        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 496        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 497        let repository_handle =
 498            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 499
 500        let message = SharedString::from(envelope.payload.message);
 501        let name = envelope.payload.name.map(SharedString::from);
 502        let email = envelope.payload.email.map(SharedString::from);
 503
 504        repository_handle
 505            .update(&mut cx, |repository_handle, _| {
 506                repository_handle.commit(message, name.zip(email))
 507            })?
 508            .await??;
 509        Ok(proto::Ack {})
 510    }
 511
 512    async fn handle_show(
 513        this: Entity<Self>,
 514        envelope: TypedEnvelope<proto::GitShow>,
 515        mut cx: AsyncApp,
 516    ) -> Result<proto::GitCommitDetails> {
 517        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 518        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 519        let repository_handle =
 520            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 521
 522        let commit = repository_handle
 523            .update(&mut cx, |repository_handle, cx| {
 524                repository_handle.show(&envelope.payload.commit, cx)
 525            })?
 526            .await?;
 527        Ok(proto::GitCommitDetails {
 528            sha: commit.sha.into(),
 529            message: commit.message.into(),
 530            commit_timestamp: commit.commit_timestamp,
 531            committer_email: commit.committer_email.into(),
 532            committer_name: commit.committer_name.into(),
 533        })
 534    }
 535
 536    async fn handle_reset(
 537        this: Entity<Self>,
 538        envelope: TypedEnvelope<proto::GitReset>,
 539        mut cx: AsyncApp,
 540    ) -> Result<proto::Ack> {
 541        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 542        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 543        let repository_handle =
 544            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 545
 546        let mode = match envelope.payload.mode() {
 547            git_reset::ResetMode::Soft => ResetMode::Soft,
 548            git_reset::ResetMode::Mixed => ResetMode::Mixed,
 549        };
 550
 551        repository_handle
 552            .update(&mut cx, |repository_handle, _| {
 553                repository_handle.reset(&envelope.payload.commit, mode)
 554            })?
 555            .await??;
 556        Ok(proto::Ack {})
 557    }
 558
 559    async fn handle_checkout_files(
 560        this: Entity<Self>,
 561        envelope: TypedEnvelope<proto::GitCheckoutFiles>,
 562        mut cx: AsyncApp,
 563    ) -> Result<proto::Ack> {
 564        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 565        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 566        let repository_handle =
 567            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 568        let paths = envelope
 569            .payload
 570            .paths
 571            .iter()
 572            .map(|s| RepoPath::from_str(s))
 573            .collect();
 574
 575        repository_handle
 576            .update(&mut cx, |repository_handle, _| {
 577                repository_handle.checkout_files(&envelope.payload.commit, paths)
 578            })?
 579            .await??;
 580        Ok(proto::Ack {})
 581    }
 582
 583    async fn handle_open_commit_message_buffer(
 584        this: Entity<Self>,
 585        envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
 586        mut cx: AsyncApp,
 587    ) -> Result<proto::OpenBufferResponse> {
 588        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 589        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 590        let repository =
 591            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 592        let buffer = repository
 593            .update(&mut cx, |repository, cx| {
 594                repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
 595            })?
 596            .await?;
 597
 598        let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?;
 599        this.update(&mut cx, |this, cx| {
 600            this.buffer_store.update(cx, |buffer_store, cx| {
 601                buffer_store
 602                    .create_buffer_for_peer(
 603                        &buffer,
 604                        envelope.original_sender_id.unwrap_or(envelope.sender_id),
 605                        cx,
 606                    )
 607                    .detach_and_log_err(cx);
 608            })
 609        })?;
 610
 611        Ok(proto::OpenBufferResponse {
 612            buffer_id: buffer_id.to_proto(),
 613        })
 614    }
 615
 616    fn repository_for_request(
 617        this: &Entity<Self>,
 618        worktree_id: WorktreeId,
 619        work_directory_id: ProjectEntryId,
 620        cx: &mut AsyncApp,
 621    ) -> Result<Entity<Repository>> {
 622        this.update(cx, |this, cx| {
 623            let repository_handle = this
 624                .all_repositories()
 625                .into_iter()
 626                .find(|repository_handle| {
 627                    repository_handle.read(cx).worktree_id == worktree_id
 628                        && repository_handle
 629                            .read(cx)
 630                            .repository_entry
 631                            .work_directory_id()
 632                            == work_directory_id
 633                })
 634                .context("missing repository handle")?;
 635            anyhow::Ok(repository_handle)
 636        })?
 637    }
 638}
 639
 640impl GitRepo {}
 641
 642impl Repository {
 643    pub fn git_store(&self) -> Option<Entity<GitStore>> {
 644        self.git_store.upgrade()
 645    }
 646
 647    fn id(&self) -> (WorktreeId, ProjectEntryId) {
 648        (self.worktree_id, self.repository_entry.work_directory_id())
 649    }
 650
 651    pub fn branch(&self) -> Option<&Branch> {
 652        self.repository_entry.branch()
 653    }
 654
 655    pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
 656        maybe!({
 657            let project_path = self.repo_path_to_project_path(&"".into())?;
 658            let worktree_name = project
 659                .worktree_for_id(project_path.worktree_id, cx)?
 660                .read(cx)
 661                .root_name();
 662
 663            let mut path = PathBuf::new();
 664            path = path.join(worktree_name);
 665            path = path.join(project_path.path);
 666            Some(path.to_string_lossy().to_string())
 667        })
 668        .unwrap_or_else(|| self.repository_entry.work_directory.display_name())
 669        .into()
 670    }
 671
 672    pub fn activate(&self, cx: &mut Context<Self>) {
 673        let Some(git_store) = self.git_store.upgrade() else {
 674            return;
 675        };
 676        let entity = cx.entity();
 677        git_store.update(cx, |git_store, cx| {
 678            let Some(index) = git_store
 679                .repositories
 680                .iter()
 681                .position(|handle| *handle == entity)
 682            else {
 683                return;
 684            };
 685            git_store.active_index = Some(index);
 686            cx.emit(GitEvent::ActiveRepositoryChanged);
 687        });
 688    }
 689
 690    pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
 691        self.repository_entry.status()
 692    }
 693
 694    pub fn has_conflict(&self, path: &RepoPath) -> bool {
 695        self.repository_entry
 696            .current_merge_conflicts
 697            .contains(&path)
 698    }
 699
 700    pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option<ProjectPath> {
 701        let path = self.repository_entry.unrelativize(path)?;
 702        Some((self.worktree_id, path).into())
 703    }
 704
 705    pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
 706        self.worktree_id_path_to_repo_path(path.worktree_id, &path.path)
 707    }
 708
 709    pub fn worktree_id_path_to_repo_path(
 710        &self,
 711        worktree_id: WorktreeId,
 712        path: &Path,
 713    ) -> Option<RepoPath> {
 714        if worktree_id != self.worktree_id {
 715            return None;
 716        }
 717        self.repository_entry.relativize(path).log_err()
 718    }
 719
 720    pub fn open_commit_buffer(
 721        &mut self,
 722        languages: Option<Arc<LanguageRegistry>>,
 723        buffer_store: Entity<BufferStore>,
 724        cx: &mut Context<Self>,
 725    ) -> Task<Result<Entity<Buffer>>> {
 726        if let Some(buffer) = self.commit_message_buffer.clone() {
 727            return Task::ready(Ok(buffer));
 728        }
 729
 730        if let GitRepo::Remote {
 731            project_id,
 732            client,
 733            worktree_id,
 734            work_directory_id,
 735        } = self.git_repo.clone()
 736        {
 737            let client = client.clone();
 738            cx.spawn(|repository, mut cx| async move {
 739                let request = client.request(proto::OpenCommitMessageBuffer {
 740                    project_id: project_id.0,
 741                    worktree_id: worktree_id.to_proto(),
 742                    work_directory_id: work_directory_id.to_proto(),
 743                });
 744                let response = request.await.context("requesting to open commit buffer")?;
 745                let buffer_id = BufferId::new(response.buffer_id)?;
 746                let buffer = buffer_store
 747                    .update(&mut cx, |buffer_store, cx| {
 748                        buffer_store.wait_for_remote_buffer(buffer_id, cx)
 749                    })?
 750                    .await?;
 751                if let Some(language_registry) = languages {
 752                    let git_commit_language =
 753                        language_registry.language_for_name("Git Commit").await?;
 754                    buffer.update(&mut cx, |buffer, cx| {
 755                        buffer.set_language(Some(git_commit_language), cx);
 756                    })?;
 757                }
 758                repository.update(&mut cx, |repository, _| {
 759                    repository.commit_message_buffer = Some(buffer.clone());
 760                })?;
 761                Ok(buffer)
 762            })
 763        } else {
 764            self.open_local_commit_buffer(languages, buffer_store, cx)
 765        }
 766    }
 767
 768    fn open_local_commit_buffer(
 769        &mut self,
 770        language_registry: Option<Arc<LanguageRegistry>>,
 771        buffer_store: Entity<BufferStore>,
 772        cx: &mut Context<Self>,
 773    ) -> Task<Result<Entity<Buffer>>> {
 774        let merge_message = self.merge_message.clone();
 775        cx.spawn(|repository, mut cx| async move {
 776            let buffer = buffer_store
 777                .update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
 778                .await?;
 779
 780            if let Some(language_registry) = language_registry {
 781                let git_commit_language = language_registry.language_for_name("Git Commit").await?;
 782                buffer.update(&mut cx, |buffer, cx| {
 783                    buffer.set_language(Some(git_commit_language), cx);
 784                })?;
 785            }
 786
 787            if let Some(merge_message) = merge_message {
 788                buffer.update(&mut cx, |buffer, cx| {
 789                    buffer.set_text(merge_message.as_str(), cx)
 790                })?;
 791            }
 792
 793            repository.update(&mut cx, |repository, _| {
 794                repository.commit_message_buffer = Some(buffer.clone());
 795            })?;
 796            Ok(buffer)
 797        })
 798    }
 799
 800    pub fn checkout_files(
 801        &self,
 802        commit: &str,
 803        paths: Vec<RepoPath>,
 804    ) -> oneshot::Receiver<Result<()>> {
 805        let (result_tx, result_rx) = futures::channel::oneshot::channel();
 806        let commit = commit.to_string().into();
 807        self.update_sender
 808            .unbounded_send((
 809                Message::CheckoutFiles {
 810                    repo: self.git_repo.clone(),
 811                    commit,
 812                    paths,
 813                },
 814                result_tx,
 815            ))
 816            .ok();
 817        result_rx
 818    }
 819
 820    pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
 821        let (result_tx, result_rx) = futures::channel::oneshot::channel();
 822        let commit = commit.to_string().into();
 823        self.update_sender
 824            .unbounded_send((
 825                Message::Reset {
 826                    repo: self.git_repo.clone(),
 827                    commit,
 828                    reset_mode,
 829                },
 830                result_tx,
 831            ))
 832            .ok();
 833        result_rx
 834    }
 835
 836    pub fn show(&self, commit: &str, cx: &Context<Self>) -> Task<Result<CommitDetails>> {
 837        let commit = commit.to_string();
 838        match self.git_repo.clone() {
 839            GitRepo::Local(git_repository) => {
 840                let commit = commit.to_string();
 841                cx.background_spawn(async move { git_repository.show(&commit) })
 842            }
 843            GitRepo::Remote {
 844                project_id,
 845                client,
 846                worktree_id,
 847                work_directory_id,
 848            } => cx.background_spawn(async move {
 849                let resp = client
 850                    .request(proto::GitShow {
 851                        project_id: project_id.0,
 852                        worktree_id: worktree_id.to_proto(),
 853                        work_directory_id: work_directory_id.to_proto(),
 854                        commit,
 855                    })
 856                    .await?;
 857
 858                Ok(CommitDetails {
 859                    sha: resp.sha.into(),
 860                    message: resp.message.into(),
 861                    commit_timestamp: resp.commit_timestamp,
 862                    committer_email: resp.committer_email.into(),
 863                    committer_name: resp.committer_name.into(),
 864                })
 865            }),
 866        }
 867    }
 868
 869    fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
 870        Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
 871    }
 872
 873    pub fn stage_entries(&self, entries: Vec<RepoPath>, cx: &mut App) -> Task<anyhow::Result<()>> {
 874        let (result_tx, result_rx) = futures::channel::oneshot::channel();
 875        if entries.is_empty() {
 876            return Task::ready(Ok(()));
 877        }
 878
 879        let mut save_futures = Vec::new();
 880        if let Some(buffer_store) = self.buffer_store(cx) {
 881            buffer_store.update(cx, |buffer_store, cx| {
 882                for path in &entries {
 883                    let Some(path) = self.repository_entry.unrelativize(path) else {
 884                        continue;
 885                    };
 886                    let project_path = (self.worktree_id, path).into();
 887                    if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) {
 888                        save_futures.push(buffer_store.save_buffer(buffer, cx));
 889                    }
 890                }
 891            })
 892        }
 893
 894        let update_sender = self.update_sender.clone();
 895        let git_repo = self.git_repo.clone();
 896        cx.spawn(|_| async move {
 897            for save_future in save_futures {
 898                save_future.await?;
 899            }
 900            update_sender
 901                .unbounded_send((Message::Stage(git_repo, entries), result_tx))
 902                .ok();
 903            result_rx.await.anyhow()??;
 904            Ok(())
 905        })
 906    }
 907
 908    pub fn unstage_entries(
 909        &self,
 910        entries: Vec<RepoPath>,
 911        cx: &mut App,
 912    ) -> Task<anyhow::Result<()>> {
 913        let (result_tx, result_rx) = futures::channel::oneshot::channel();
 914        if entries.is_empty() {
 915            return Task::ready(Ok(()));
 916        }
 917
 918        let mut save_futures = Vec::new();
 919        if let Some(buffer_store) = self.buffer_store(cx) {
 920            buffer_store.update(cx, |buffer_store, cx| {
 921                for path in &entries {
 922                    let Some(path) = self.repository_entry.unrelativize(path) else {
 923                        continue;
 924                    };
 925                    let project_path = (self.worktree_id, path).into();
 926                    if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) {
 927                        save_futures.push(buffer_store.save_buffer(buffer, cx));
 928                    }
 929                }
 930            })
 931        }
 932
 933        let update_sender = self.update_sender.clone();
 934        let git_repo = self.git_repo.clone();
 935        cx.spawn(|_| async move {
 936            for save_future in save_futures {
 937                save_future.await?;
 938            }
 939            update_sender
 940                .unbounded_send((Message::Unstage(git_repo, entries), result_tx))
 941                .ok();
 942            result_rx.await.anyhow()??;
 943            Ok(())
 944        })
 945    }
 946
 947    pub fn stage_all(&self, cx: &mut App) -> Task<anyhow::Result<()>> {
 948        let to_stage = self
 949            .repository_entry
 950            .status()
 951            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
 952            .map(|entry| entry.repo_path.clone())
 953            .collect();
 954        self.stage_entries(to_stage, cx)
 955    }
 956
 957    pub fn unstage_all(&self, cx: &mut App) -> Task<anyhow::Result<()>> {
 958        let to_unstage = self
 959            .repository_entry
 960            .status()
 961            .filter(|entry| entry.status.is_staged().unwrap_or(true))
 962            .map(|entry| entry.repo_path.clone())
 963            .collect();
 964        self.unstage_entries(to_unstage, cx)
 965    }
 966
 967    /// Get a count of all entries in the active repository, including
 968    /// untracked files.
 969    pub fn entry_count(&self) -> usize {
 970        self.repository_entry.status_len()
 971    }
 972
 973    fn have_changes(&self) -> bool {
 974        self.repository_entry.status_summary() != GitSummary::UNCHANGED
 975    }
 976
 977    fn have_staged_changes(&self) -> bool {
 978        self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
 979    }
 980
 981    pub fn can_commit(&self, commit_all: bool) -> bool {
 982        return self.have_changes() && (commit_all || self.have_staged_changes());
 983    }
 984
 985    pub fn commit(
 986        &self,
 987        message: SharedString,
 988        name_and_email: Option<(SharedString, SharedString)>,
 989    ) -> oneshot::Receiver<Result<()>> {
 990        let (result_tx, result_rx) = futures::channel::oneshot::channel();
 991        self.update_sender
 992            .unbounded_send((
 993                Message::Commit {
 994                    git_repo: self.git_repo.clone(),
 995                    message,
 996                    name_and_email,
 997                },
 998                result_tx,
 999            ))
1000            .ok();
1001        result_rx
1002    }
1003
1004    pub fn set_index_text(
1005        &self,
1006        path: &RepoPath,
1007        content: Option<String>,
1008    ) -> oneshot::Receiver<anyhow::Result<()>> {
1009        let (result_tx, result_rx) = futures::channel::oneshot::channel();
1010        self.update_sender
1011            .unbounded_send((
1012                Message::SetIndexText(self.git_repo.clone(), path.clone(), content),
1013                result_tx,
1014            ))
1015            .ok();
1016        result_rx
1017    }
1018}