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