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