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, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
  15    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
 246                    .background_executor()
 247                    .spawn(Self::process_git_msg(msg))
 248                    .await;
 249                respond.send(result).ok();
 250            }
 251        })
 252        .detach();
 253        update_sender
 254    }
 255
 256    async fn process_git_msg(msg: Message) -> Result<()> {
 257        match msg {
 258            Message::Stage(repo, paths) => {
 259                match repo {
 260                    GitRepo::Local(repo) => repo.stage_paths(&paths)?,
 261                    GitRepo::Remote {
 262                        project_id,
 263                        client,
 264                        worktree_id,
 265                        work_directory_id,
 266                    } => {
 267                        client
 268                            .request(proto::Stage {
 269                                project_id: project_id.0,
 270                                worktree_id: worktree_id.to_proto(),
 271                                work_directory_id: work_directory_id.to_proto(),
 272                                paths: paths
 273                                    .into_iter()
 274                                    .map(|repo_path| repo_path.as_ref().to_proto())
 275                                    .collect(),
 276                            })
 277                            .await
 278                            .context("sending stage request")?;
 279                    }
 280                }
 281                Ok(())
 282            }
 283            Message::Reset {
 284                repo,
 285                commit,
 286                reset_mode,
 287            } => {
 288                match repo {
 289                    GitRepo::Local(repo) => repo.reset(&commit, reset_mode)?,
 290                    GitRepo::Remote {
 291                        project_id,
 292                        client,
 293                        worktree_id,
 294                        work_directory_id,
 295                    } => {
 296                        client
 297                            .request(proto::GitReset {
 298                                project_id: project_id.0,
 299                                worktree_id: worktree_id.to_proto(),
 300                                work_directory_id: work_directory_id.to_proto(),
 301                                commit: commit.into(),
 302                                mode: match reset_mode {
 303                                    ResetMode::Soft => git_reset::ResetMode::Soft.into(),
 304                                    ResetMode::Mixed => git_reset::ResetMode::Mixed.into(),
 305                                },
 306                            })
 307                            .await?;
 308                    }
 309                }
 310                Ok(())
 311            }
 312
 313            Message::CheckoutFiles {
 314                repo,
 315                commit,
 316                paths,
 317            } => {
 318                match repo {
 319                    GitRepo::Local(repo) => repo.checkout_files(&commit, &paths)?,
 320                    GitRepo::Remote {
 321                        project_id,
 322                        client,
 323                        worktree_id,
 324                        work_directory_id,
 325                    } => {
 326                        client
 327                            .request(proto::GitCheckoutFiles {
 328                                project_id: project_id.0,
 329                                worktree_id: worktree_id.to_proto(),
 330                                work_directory_id: work_directory_id.to_proto(),
 331                                commit: commit.into(),
 332                                paths: paths
 333                                    .into_iter()
 334                                    .map(|p| p.to_string_lossy().to_string())
 335                                    .collect(),
 336                            })
 337                            .await?;
 338                    }
 339                }
 340                Ok(())
 341            }
 342            Message::Unstage(repo, paths) => {
 343                match repo {
 344                    GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
 345                    GitRepo::Remote {
 346                        project_id,
 347                        client,
 348                        worktree_id,
 349                        work_directory_id,
 350                    } => {
 351                        client
 352                            .request(proto::Unstage {
 353                                project_id: project_id.0,
 354                                worktree_id: worktree_id.to_proto(),
 355                                work_directory_id: work_directory_id.to_proto(),
 356                                paths: paths
 357                                    .into_iter()
 358                                    .map(|repo_path| repo_path.as_ref().to_proto())
 359                                    .collect(),
 360                            })
 361                            .await
 362                            .context("sending unstage request")?;
 363                    }
 364                }
 365                Ok(())
 366            }
 367            Message::Commit {
 368                git_repo,
 369                message,
 370                name_and_email,
 371            } => {
 372                match git_repo {
 373                    GitRepo::Local(repo) => repo.commit(
 374                        message.as_ref(),
 375                        name_and_email
 376                            .as_ref()
 377                            .map(|(name, email)| (name.as_ref(), email.as_ref())),
 378                    )?,
 379                    GitRepo::Remote {
 380                        project_id,
 381                        client,
 382                        worktree_id,
 383                        work_directory_id,
 384                    } => {
 385                        let (name, email) = name_and_email.unzip();
 386                        client
 387                            .request(proto::Commit {
 388                                project_id: project_id.0,
 389                                worktree_id: worktree_id.to_proto(),
 390                                work_directory_id: work_directory_id.to_proto(),
 391                                message: String::from(message),
 392                                name: name.map(String::from),
 393                                email: email.map(String::from),
 394                            })
 395                            .await
 396                            .context("sending commit request")?;
 397                    }
 398                }
 399                Ok(())
 400            }
 401            Message::SetIndexText(git_repo, path, text) => match git_repo {
 402                GitRepo::Local(repo) => repo.set_index_text(&path, text),
 403                GitRepo::Remote {
 404                    project_id,
 405                    client,
 406                    worktree_id,
 407                    work_directory_id,
 408                } => client.send(proto::SetIndexText {
 409                    project_id: project_id.0,
 410                    worktree_id: worktree_id.to_proto(),
 411                    work_directory_id: work_directory_id.to_proto(),
 412                    path: path.as_ref().to_proto(),
 413                    text,
 414                }),
 415            },
 416        }
 417    }
 418
 419    async fn handle_stage(
 420        this: Entity<Self>,
 421        envelope: TypedEnvelope<proto::Stage>,
 422        mut cx: AsyncApp,
 423    ) -> Result<proto::Ack> {
 424        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 425        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 426        let repository_handle =
 427            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 428
 429        let entries = envelope
 430            .payload
 431            .paths
 432            .into_iter()
 433            .map(PathBuf::from)
 434            .map(RepoPath::new)
 435            .collect();
 436
 437        repository_handle
 438            .update(&mut cx, |repository_handle, cx| {
 439                repository_handle.stage_entries(entries, cx)
 440            })?
 441            .await?;
 442        Ok(proto::Ack {})
 443    }
 444
 445    async fn handle_unstage(
 446        this: Entity<Self>,
 447        envelope: TypedEnvelope<proto::Unstage>,
 448        mut cx: AsyncApp,
 449    ) -> Result<proto::Ack> {
 450        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 451        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 452        let repository_handle =
 453            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 454
 455        let entries = envelope
 456            .payload
 457            .paths
 458            .into_iter()
 459            .map(PathBuf::from)
 460            .map(RepoPath::new)
 461            .collect();
 462
 463        repository_handle
 464            .update(&mut cx, |repository_handle, cx| {
 465                repository_handle.unstage_entries(entries, cx)
 466            })?
 467            .await?;
 468
 469        Ok(proto::Ack {})
 470    }
 471
 472    async fn handle_set_index_text(
 473        this: Entity<Self>,
 474        envelope: TypedEnvelope<proto::SetIndexText>,
 475        mut cx: AsyncApp,
 476    ) -> Result<proto::Ack> {
 477        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 478        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 479        let repository_handle =
 480            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 481
 482        repository_handle
 483            .update(&mut cx, |repository_handle, _| {
 484                repository_handle.set_index_text(
 485                    &RepoPath::from_str(&envelope.payload.path),
 486                    envelope.payload.text,
 487                )
 488            })?
 489            .await??;
 490        Ok(proto::Ack {})
 491    }
 492
 493    async fn handle_commit(
 494        this: Entity<Self>,
 495        envelope: TypedEnvelope<proto::Commit>,
 496        mut cx: AsyncApp,
 497    ) -> Result<proto::Ack> {
 498        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 499        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 500        let repository_handle =
 501            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 502
 503        let message = SharedString::from(envelope.payload.message);
 504        let name = envelope.payload.name.map(SharedString::from);
 505        let email = envelope.payload.email.map(SharedString::from);
 506
 507        repository_handle
 508            .update(&mut cx, |repository_handle, _| {
 509                repository_handle.commit(message, name.zip(email))
 510            })?
 511            .await??;
 512        Ok(proto::Ack {})
 513    }
 514
 515    async fn handle_show(
 516        this: Entity<Self>,
 517        envelope: TypedEnvelope<proto::GitShow>,
 518        mut cx: AsyncApp,
 519    ) -> Result<proto::GitCommitDetails> {
 520        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 521        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 522        let repository_handle =
 523            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 524
 525        let commit = repository_handle
 526            .update(&mut cx, |repository_handle, cx| {
 527                repository_handle.show(&envelope.payload.commit, cx)
 528            })?
 529            .await?;
 530        Ok(proto::GitCommitDetails {
 531            sha: commit.sha.into(),
 532            message: commit.message.into(),
 533            commit_timestamp: commit.commit_timestamp,
 534            committer_email: commit.committer_email.into(),
 535            committer_name: commit.committer_name.into(),
 536        })
 537    }
 538
 539    async fn handle_reset(
 540        this: Entity<Self>,
 541        envelope: TypedEnvelope<proto::GitReset>,
 542        mut cx: AsyncApp,
 543    ) -> Result<proto::Ack> {
 544        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 545        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 546        let repository_handle =
 547            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 548
 549        let mode = match envelope.payload.mode() {
 550            git_reset::ResetMode::Soft => ResetMode::Soft,
 551            git_reset::ResetMode::Mixed => ResetMode::Mixed,
 552        };
 553
 554        repository_handle
 555            .update(&mut cx, |repository_handle, _| {
 556                repository_handle.reset(&envelope.payload.commit, mode)
 557            })?
 558            .await??;
 559        Ok(proto::Ack {})
 560    }
 561
 562    async fn handle_checkout_files(
 563        this: Entity<Self>,
 564        envelope: TypedEnvelope<proto::GitCheckoutFiles>,
 565        mut cx: AsyncApp,
 566    ) -> Result<proto::Ack> {
 567        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 568        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 569        let repository_handle =
 570            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 571        let paths = envelope
 572            .payload
 573            .paths
 574            .iter()
 575            .map(|s| RepoPath::from_str(s))
 576            .collect();
 577
 578        repository_handle
 579            .update(&mut cx, |repository_handle, _| {
 580                repository_handle.checkout_files(&envelope.payload.commit, paths)
 581            })?
 582            .await??;
 583        Ok(proto::Ack {})
 584    }
 585
 586    async fn handle_open_commit_message_buffer(
 587        this: Entity<Self>,
 588        envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
 589        mut cx: AsyncApp,
 590    ) -> Result<proto::OpenBufferResponse> {
 591        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 592        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
 593        let repository =
 594            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 595        let buffer = repository
 596            .update(&mut cx, |repository, cx| {
 597                repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
 598            })?
 599            .await?;
 600
 601        let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?;
 602        this.update(&mut cx, |this, cx| {
 603            this.buffer_store.update(cx, |buffer_store, cx| {
 604                buffer_store
 605                    .create_buffer_for_peer(
 606                        &buffer,
 607                        envelope.original_sender_id.unwrap_or(envelope.sender_id),
 608                        cx,
 609                    )
 610                    .detach_and_log_err(cx);
 611            })
 612        })?;
 613
 614        Ok(proto::OpenBufferResponse {
 615            buffer_id: buffer_id.to_proto(),
 616        })
 617    }
 618
 619    fn repository_for_request(
 620        this: &Entity<Self>,
 621        worktree_id: WorktreeId,
 622        work_directory_id: ProjectEntryId,
 623        cx: &mut AsyncApp,
 624    ) -> Result<Entity<Repository>> {
 625        this.update(cx, |this, cx| {
 626            let repository_handle = this
 627                .all_repositories()
 628                .into_iter()
 629                .find(|repository_handle| {
 630                    repository_handle.read(cx).worktree_id == worktree_id
 631                        && repository_handle
 632                            .read(cx)
 633                            .repository_entry
 634                            .work_directory_id()
 635                            == work_directory_id
 636                })
 637                .context("missing repository handle")?;
 638            anyhow::Ok(repository_handle)
 639        })?
 640    }
 641}
 642
 643impl GitRepo {}
 644
 645impl Repository {
 646    pub fn git_store(&self) -> Option<Entity<GitStore>> {
 647        self.git_store.upgrade()
 648    }
 649
 650    fn id(&self) -> (WorktreeId, ProjectEntryId) {
 651        (self.worktree_id, self.repository_entry.work_directory_id())
 652    }
 653
 654    pub fn branch(&self) -> Option<&Branch> {
 655        self.repository_entry.branch()
 656    }
 657
 658    pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
 659        maybe!({
 660            let project_path = self.repo_path_to_project_path(&"".into())?;
 661            let worktree_name = project
 662                .worktree_for_id(project_path.worktree_id, cx)?
 663                .read(cx)
 664                .root_name();
 665
 666            let mut path = PathBuf::new();
 667            path = path.join(worktree_name);
 668            path = path.join(project_path.path);
 669            Some(path.to_string_lossy().to_string())
 670        })
 671        .unwrap_or_else(|| self.repository_entry.work_directory.display_name())
 672        .into()
 673    }
 674
 675    pub fn activate(&self, cx: &mut Context<Self>) {
 676        let Some(git_store) = self.git_store.upgrade() else {
 677            return;
 678        };
 679        let entity = cx.entity();
 680        git_store.update(cx, |git_store, cx| {
 681            let Some(index) = git_store
 682                .repositories
 683                .iter()
 684                .position(|handle| *handle == entity)
 685            else {
 686                return;
 687            };
 688            git_store.active_index = Some(index);
 689            cx.emit(GitEvent::ActiveRepositoryChanged);
 690        });
 691    }
 692
 693    pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
 694        self.repository_entry.status()
 695    }
 696
 697    pub fn has_conflict(&self, path: &RepoPath) -> bool {
 698        self.repository_entry
 699            .current_merge_conflicts
 700            .contains(&path)
 701    }
 702
 703    pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option<ProjectPath> {
 704        let path = self.repository_entry.unrelativize(path)?;
 705        Some((self.worktree_id, path).into())
 706    }
 707
 708    pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
 709        self.worktree_id_path_to_repo_path(path.worktree_id, &path.path)
 710    }
 711
 712    pub fn worktree_id_path_to_repo_path(
 713        &self,
 714        worktree_id: WorktreeId,
 715        path: &Path,
 716    ) -> Option<RepoPath> {
 717        if worktree_id != self.worktree_id {
 718            return None;
 719        }
 720        self.repository_entry.relativize(path).log_err()
 721    }
 722
 723    pub fn open_commit_buffer(
 724        &mut self,
 725        languages: Option<Arc<LanguageRegistry>>,
 726        buffer_store: Entity<BufferStore>,
 727        cx: &mut Context<Self>,
 728    ) -> Task<Result<Entity<Buffer>>> {
 729        if let Some(buffer) = self.commit_message_buffer.clone() {
 730            return Task::ready(Ok(buffer));
 731        }
 732
 733        if let GitRepo::Remote {
 734            project_id,
 735            client,
 736            worktree_id,
 737            work_directory_id,
 738        } = self.git_repo.clone()
 739        {
 740            let client = client.clone();
 741            cx.spawn(|repository, mut cx| async move {
 742                let request = client.request(proto::OpenCommitMessageBuffer {
 743                    project_id: project_id.0,
 744                    worktree_id: worktree_id.to_proto(),
 745                    work_directory_id: work_directory_id.to_proto(),
 746                });
 747                let response = request.await.context("requesting to open commit buffer")?;
 748                let buffer_id = BufferId::new(response.buffer_id)?;
 749                let buffer = buffer_store
 750                    .update(&mut cx, |buffer_store, cx| {
 751                        buffer_store.wait_for_remote_buffer(buffer_id, cx)
 752                    })?
 753                    .await?;
 754                if let Some(language_registry) = languages {
 755                    let git_commit_language =
 756                        language_registry.language_for_name("Git Commit").await?;
 757                    buffer.update(&mut cx, |buffer, cx| {
 758                        buffer.set_language(Some(git_commit_language), cx);
 759                    })?;
 760                }
 761                repository.update(&mut cx, |repository, _| {
 762                    repository.commit_message_buffer = Some(buffer.clone());
 763                })?;
 764                Ok(buffer)
 765            })
 766        } else {
 767            self.open_local_commit_buffer(languages, buffer_store, cx)
 768        }
 769    }
 770
 771    fn open_local_commit_buffer(
 772        &mut self,
 773        language_registry: Option<Arc<LanguageRegistry>>,
 774        buffer_store: Entity<BufferStore>,
 775        cx: &mut Context<Self>,
 776    ) -> Task<Result<Entity<Buffer>>> {
 777        let merge_message = self.merge_message.clone();
 778        cx.spawn(|repository, mut cx| async move {
 779            let buffer = buffer_store
 780                .update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
 781                .await?;
 782
 783            if let Some(language_registry) = language_registry {
 784                let git_commit_language = language_registry.language_for_name("Git Commit").await?;
 785                buffer.update(&mut cx, |buffer, cx| {
 786                    buffer.set_language(Some(git_commit_language), cx);
 787                })?;
 788            }
 789
 790            if let Some(merge_message) = merge_message {
 791                buffer.update(&mut cx, |buffer, cx| {
 792                    buffer.set_text(merge_message.as_str(), cx)
 793                })?;
 794            }
 795
 796            repository.update(&mut cx, |repository, _| {
 797                repository.commit_message_buffer = Some(buffer.clone());
 798            })?;
 799            Ok(buffer)
 800        })
 801    }
 802
 803    pub fn checkout_files(
 804        &self,
 805        commit: &str,
 806        paths: Vec<RepoPath>,
 807    ) -> oneshot::Receiver<Result<()>> {
 808        let (result_tx, result_rx) = futures::channel::oneshot::channel();
 809        let commit = commit.to_string().into();
 810        self.update_sender
 811            .unbounded_send((
 812                Message::CheckoutFiles {
 813                    repo: self.git_repo.clone(),
 814                    commit,
 815                    paths,
 816                },
 817                result_tx,
 818            ))
 819            .ok();
 820        result_rx
 821    }
 822
 823    pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
 824        let (result_tx, result_rx) = futures::channel::oneshot::channel();
 825        let commit = commit.to_string().into();
 826        self.update_sender
 827            .unbounded_send((
 828                Message::Reset {
 829                    repo: self.git_repo.clone(),
 830                    commit,
 831                    reset_mode,
 832                },
 833                result_tx,
 834            ))
 835            .ok();
 836        result_rx
 837    }
 838
 839    pub fn show(&self, commit: &str, cx: &Context<Self>) -> Task<Result<CommitDetails>> {
 840        let commit = commit.to_string();
 841        match self.git_repo.clone() {
 842            GitRepo::Local(git_repository) => {
 843                let commit = commit.to_string();
 844                cx.background_executor()
 845                    .spawn(async move { git_repository.show(&commit) })
 846            }
 847            GitRepo::Remote {
 848                project_id,
 849                client,
 850                worktree_id,
 851                work_directory_id,
 852            } => cx.background_executor().spawn(async move {
 853                let resp = client
 854                    .request(proto::GitShow {
 855                        project_id: project_id.0,
 856                        worktree_id: worktree_id.to_proto(),
 857                        work_directory_id: work_directory_id.to_proto(),
 858                        commit,
 859                    })
 860                    .await?;
 861
 862                Ok(CommitDetails {
 863                    sha: resp.sha.into(),
 864                    message: resp.message.into(),
 865                    commit_timestamp: resp.commit_timestamp,
 866                    committer_email: resp.committer_email.into(),
 867                    committer_name: resp.committer_name.into(),
 868                })
 869            }),
 870        }
 871    }
 872
 873    fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
 874        Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
 875    }
 876
 877    pub fn stage_entries(&self, entries: Vec<RepoPath>, cx: &mut App) -> Task<anyhow::Result<()>> {
 878        let (result_tx, result_rx) = futures::channel::oneshot::channel();
 879        if entries.is_empty() {
 880            return Task::ready(Ok(()));
 881        }
 882
 883        let mut save_futures = Vec::new();
 884        if let Some(buffer_store) = self.buffer_store(cx) {
 885            buffer_store.update(cx, |buffer_store, cx| {
 886                for path in &entries {
 887                    let Some(path) = self.repository_entry.unrelativize(path) else {
 888                        continue;
 889                    };
 890                    let project_path = (self.worktree_id, path).into();
 891                    if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) {
 892                        save_futures.push(buffer_store.save_buffer(buffer, cx));
 893                    }
 894                }
 895            })
 896        }
 897
 898        let update_sender = self.update_sender.clone();
 899        let git_repo = self.git_repo.clone();
 900        cx.spawn(|_| async move {
 901            for save_future in save_futures {
 902                save_future.await?;
 903            }
 904            update_sender
 905                .unbounded_send((Message::Stage(git_repo, entries), result_tx))
 906                .ok();
 907            result_rx.await.anyhow()??;
 908            Ok(())
 909        })
 910    }
 911
 912    pub fn unstage_entries(
 913        &self,
 914        entries: Vec<RepoPath>,
 915        cx: &mut App,
 916    ) -> Task<anyhow::Result<()>> {
 917        let (result_tx, result_rx) = futures::channel::oneshot::channel();
 918        if entries.is_empty() {
 919            return Task::ready(Ok(()));
 920        }
 921
 922        let mut save_futures = Vec::new();
 923        if let Some(buffer_store) = self.buffer_store(cx) {
 924            buffer_store.update(cx, |buffer_store, cx| {
 925                for path in &entries {
 926                    let Some(path) = self.repository_entry.unrelativize(path) else {
 927                        continue;
 928                    };
 929                    let project_path = (self.worktree_id, path).into();
 930                    if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) {
 931                        save_futures.push(buffer_store.save_buffer(buffer, cx));
 932                    }
 933                }
 934            })
 935        }
 936
 937        let update_sender = self.update_sender.clone();
 938        let git_repo = self.git_repo.clone();
 939        cx.spawn(|_| async move {
 940            for save_future in save_futures {
 941                save_future.await?;
 942            }
 943            update_sender
 944                .unbounded_send((Message::Unstage(git_repo, entries), result_tx))
 945                .ok();
 946            result_rx.await.anyhow()??;
 947            Ok(())
 948        })
 949    }
 950
 951    pub fn stage_all(&self, cx: &mut App) -> Task<anyhow::Result<()>> {
 952        let to_stage = self
 953            .repository_entry
 954            .status()
 955            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
 956            .map(|entry| entry.repo_path.clone())
 957            .collect();
 958        self.stage_entries(to_stage, cx)
 959    }
 960
 961    pub fn unstage_all(&self, cx: &mut App) -> Task<anyhow::Result<()>> {
 962        let to_unstage = self
 963            .repository_entry
 964            .status()
 965            .filter(|entry| entry.status.is_staged().unwrap_or(true))
 966            .map(|entry| entry.repo_path.clone())
 967            .collect();
 968        self.unstage_entries(to_unstage, cx)
 969    }
 970
 971    /// Get a count of all entries in the active repository, including
 972    /// untracked files.
 973    pub fn entry_count(&self) -> usize {
 974        self.repository_entry.status_len()
 975    }
 976
 977    fn have_changes(&self) -> bool {
 978        self.repository_entry.status_summary() != GitSummary::UNCHANGED
 979    }
 980
 981    fn have_staged_changes(&self) -> bool {
 982        self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
 983    }
 984
 985    pub fn can_commit(&self, commit_all: bool) -> bool {
 986        return self.have_changes() && (commit_all || self.have_staged_changes());
 987    }
 988
 989    pub fn commit(
 990        &self,
 991        message: SharedString,
 992        name_and_email: Option<(SharedString, SharedString)>,
 993    ) -> oneshot::Receiver<Result<()>> {
 994        let (result_tx, result_rx) = futures::channel::oneshot::channel();
 995        self.update_sender
 996            .unbounded_send((
 997                Message::Commit {
 998                    git_repo: self.git_repo.clone(),
 999                    message,
1000                    name_and_email,
1001                },
1002                result_tx,
1003            ))
1004            .ok();
1005        result_rx
1006    }
1007
1008    pub fn set_index_text(
1009        &self,
1010        path: &RepoPath,
1011        content: Option<String>,
1012    ) -> oneshot::Receiver<anyhow::Result<()>> {
1013        let (result_tx, result_rx) = futures::channel::oneshot::channel();
1014        self.update_sender
1015            .unbounded_send((
1016                Message::SetIndexText(self.git_repo.clone(), path.clone(), content),
1017                result_tx,
1018            ))
1019            .ok();
1020        result_rx
1021    }
1022}