git.rs

  1use crate::buffer_store::BufferStore;
  2use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
  3use crate::{Project, ProjectPath};
  4use anyhow::Context as _;
  5use client::ProjectId;
  6use futures::channel::{mpsc, oneshot};
  7use futures::StreamExt as _;
  8use git::{
  9    repository::{GitRepository, RepoPath},
 10    status::{GitSummary, TrackedSummary},
 11};
 12use gpui::{
 13    App, AppContext, Context, Entity, EventEmitter, SharedString, Subscription, Task, WeakEntity,
 14};
 15use language::{Buffer, LanguageRegistry};
 16use rpc::{proto, AnyProtoClient};
 17use settings::WorktreeId;
 18use std::sync::Arc;
 19use text::BufferId;
 20use util::{maybe, ResultExt};
 21use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
 22
 23pub struct GitState {
 24    project_id: Option<ProjectId>,
 25    client: Option<AnyProtoClient>,
 26    repositories: Vec<Entity<Repository>>,
 27    active_index: Option<usize>,
 28    update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
 29    _subscription: Subscription,
 30}
 31
 32pub struct Repository {
 33    commit_message_buffer: Option<Entity<Buffer>>,
 34    git_state: WeakEntity<GitState>,
 35    pub worktree_id: WorktreeId,
 36    pub repository_entry: RepositoryEntry,
 37    pub git_repo: GitRepo,
 38    update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
 39}
 40
 41#[derive(Clone)]
 42pub enum GitRepo {
 43    Local(Arc<dyn GitRepository>),
 44    Remote {
 45        project_id: ProjectId,
 46        client: AnyProtoClient,
 47        worktree_id: WorktreeId,
 48        work_directory_id: ProjectEntryId,
 49    },
 50}
 51
 52enum Message {
 53    Commit {
 54        git_repo: GitRepo,
 55        message: SharedString,
 56        name_and_email: Option<(SharedString, SharedString)>,
 57    },
 58    Stage(GitRepo, Vec<RepoPath>),
 59    Unstage(GitRepo, Vec<RepoPath>),
 60}
 61
 62pub enum GitEvent {
 63    ActiveRepositoryChanged,
 64    FileSystemUpdated,
 65    GitStateUpdated,
 66}
 67
 68impl EventEmitter<GitEvent> for GitState {}
 69
 70impl GitState {
 71    pub fn new(
 72        worktree_store: &Entity<WorktreeStore>,
 73        client: Option<AnyProtoClient>,
 74        project_id: Option<ProjectId>,
 75        cx: &mut Context<'_, Self>,
 76    ) -> Self {
 77        let update_sender = Self::spawn_git_worker(cx);
 78        let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
 79
 80        GitState {
 81            project_id,
 82            client,
 83            repositories: Vec::new(),
 84            active_index: None,
 85            update_sender,
 86            _subscription,
 87        }
 88    }
 89
 90    pub fn active_repository(&self) -> Option<Entity<Repository>> {
 91        self.active_index
 92            .map(|index| self.repositories[index].clone())
 93    }
 94
 95    fn on_worktree_store_event(
 96        &mut self,
 97        worktree_store: Entity<WorktreeStore>,
 98        event: &WorktreeStoreEvent,
 99        cx: &mut Context<'_, Self>,
100    ) {
101        // TODO inspect the event
102
103        let mut new_repositories = Vec::new();
104        let mut new_active_index = None;
105        let this = cx.weak_entity();
106        let client = self.client.clone();
107        let project_id = self.project_id;
108
109        worktree_store.update(cx, |worktree_store, cx| {
110            for worktree in worktree_store.worktrees() {
111                worktree.update(cx, |worktree, cx| {
112                    let snapshot = worktree.snapshot();
113                    for repo in snapshot.repositories().iter() {
114                        let git_repo = worktree
115                            .as_local()
116                            .and_then(|local_worktree| local_worktree.get_local_repo(repo))
117                            .map(|local_repo| local_repo.repo().clone())
118                            .map(GitRepo::Local)
119                            .or_else(|| {
120                                let client = client.clone()?;
121                                let project_id = project_id?;
122                                Some(GitRepo::Remote {
123                                    project_id,
124                                    client,
125                                    worktree_id: worktree.id(),
126                                    work_directory_id: repo.work_directory_id(),
127                                })
128                            });
129                        let Some(git_repo) = git_repo else {
130                            continue;
131                        };
132                        let worktree_id = worktree.id();
133                        let existing =
134                            self.repositories
135                                .iter()
136                                .enumerate()
137                                .find(|(_, existing_handle)| {
138                                    existing_handle.read(cx).id()
139                                        == (worktree_id, repo.work_directory_id())
140                                });
141                        let handle = if let Some((index, handle)) = existing {
142                            if self.active_index == Some(index) {
143                                new_active_index = Some(new_repositories.len());
144                            }
145                            // Update the statuses but keep everything else.
146                            let existing_handle = handle.clone();
147                            existing_handle.update(cx, |existing_handle, _| {
148                                existing_handle.repository_entry = repo.clone();
149                            });
150                            existing_handle
151                        } else {
152                            cx.new(|_| Repository {
153                                git_state: this.clone(),
154                                worktree_id,
155                                repository_entry: repo.clone(),
156                                git_repo,
157                                update_sender: self.update_sender.clone(),
158                                commit_message_buffer: None,
159                            })
160                        };
161                        new_repositories.push(handle);
162                    }
163                })
164            }
165        });
166
167        if new_active_index == None && new_repositories.len() > 0 {
168            new_active_index = Some(0);
169        }
170
171        self.repositories = new_repositories;
172        self.active_index = new_active_index;
173
174        match event {
175            WorktreeStoreEvent::WorktreeUpdatedGitRepositories(_) => {
176                cx.emit(GitEvent::GitStateUpdated);
177            }
178            _ => {
179                cx.emit(GitEvent::FileSystemUpdated);
180            }
181        }
182    }
183
184    pub fn all_repositories(&self) -> Vec<Entity<Repository>> {
185        self.repositories.clone()
186    }
187
188    fn spawn_git_worker(
189        cx: &mut Context<'_, GitState>,
190    ) -> mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)> {
191        let (update_sender, mut update_receiver) =
192            mpsc::unbounded::<(Message, oneshot::Sender<anyhow::Result<()>>)>();
193        cx.spawn(|_, cx| async move {
194            while let Some((msg, respond)) = update_receiver.next().await {
195                let result = cx
196                    .background_executor()
197                    .spawn(Self::process_git_msg(msg))
198                    .await;
199                respond.send(result).ok();
200            }
201        })
202        .detach();
203        update_sender
204    }
205
206    async fn process_git_msg(msg: Message) -> Result<(), anyhow::Error> {
207        match msg {
208            Message::Stage(repo, paths) => {
209                match repo {
210                    GitRepo::Local(repo) => repo.stage_paths(&paths)?,
211                    GitRepo::Remote {
212                        project_id,
213                        client,
214                        worktree_id,
215                        work_directory_id,
216                    } => {
217                        client
218                            .request(proto::Stage {
219                                project_id: project_id.0,
220                                worktree_id: worktree_id.to_proto(),
221                                work_directory_id: work_directory_id.to_proto(),
222                                paths: paths
223                                    .into_iter()
224                                    .map(|repo_path| repo_path.to_proto())
225                                    .collect(),
226                            })
227                            .await
228                            .context("sending stage request")?;
229                    }
230                }
231                Ok(())
232            }
233            Message::Unstage(repo, paths) => {
234                match repo {
235                    GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
236                    GitRepo::Remote {
237                        project_id,
238                        client,
239                        worktree_id,
240                        work_directory_id,
241                    } => {
242                        client
243                            .request(proto::Unstage {
244                                project_id: project_id.0,
245                                worktree_id: worktree_id.to_proto(),
246                                work_directory_id: work_directory_id.to_proto(),
247                                paths: paths
248                                    .into_iter()
249                                    .map(|repo_path| repo_path.to_proto())
250                                    .collect(),
251                            })
252                            .await
253                            .context("sending unstage request")?;
254                    }
255                }
256                Ok(())
257            }
258            Message::Commit {
259                git_repo,
260                message,
261                name_and_email,
262            } => {
263                match git_repo {
264                    GitRepo::Local(repo) => repo.commit(
265                        message.as_ref(),
266                        name_and_email
267                            .as_ref()
268                            .map(|(name, email)| (name.as_ref(), email.as_ref())),
269                    )?,
270                    GitRepo::Remote {
271                        project_id,
272                        client,
273                        worktree_id,
274                        work_directory_id,
275                    } => {
276                        let (name, email) = name_and_email.unzip();
277                        client
278                            .request(proto::Commit {
279                                project_id: project_id.0,
280                                worktree_id: worktree_id.to_proto(),
281                                work_directory_id: work_directory_id.to_proto(),
282                                message: String::from(message),
283                                name: name.map(String::from),
284                                email: email.map(String::from),
285                            })
286                            .await
287                            .context("sending commit request")?;
288                    }
289                }
290                Ok(())
291            }
292        }
293    }
294}
295
296impl Repository {
297    fn id(&self) -> (WorktreeId, ProjectEntryId) {
298        (self.worktree_id, self.repository_entry.work_directory_id())
299    }
300
301    pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
302        maybe!({
303            let path = self.repo_path_to_project_path(&"".into())?;
304            Some(
305                project
306                    .absolute_path(&path, cx)?
307                    .file_name()?
308                    .to_string_lossy()
309                    .to_string()
310                    .into(),
311            )
312        })
313        .unwrap_or("".into())
314    }
315
316    pub fn activate(&self, cx: &mut App) {
317        let Some(git_state) = self.git_state.upgrade() else {
318            return;
319        };
320        git_state.update(cx, |git_state, cx| {
321            let Some((index, _)) = git_state
322                .repositories
323                .iter()
324                .enumerate()
325                .find(|(_, handle)| handle.read(cx).id() == self.id())
326            else {
327                return;
328            };
329            git_state.active_index = Some(index);
330            cx.emit(GitEvent::ActiveRepositoryChanged);
331        });
332    }
333
334    pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
335        self.repository_entry.status()
336    }
337
338    pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option<ProjectPath> {
339        let path = self.repository_entry.unrelativize(path)?;
340        Some((self.worktree_id, path).into())
341    }
342
343    pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
344        if path.worktree_id != self.worktree_id {
345            return None;
346        }
347        self.repository_entry.relativize(&path.path).log_err()
348    }
349
350    pub fn open_commit_buffer(
351        &mut self,
352        languages: Option<Arc<LanguageRegistry>>,
353        buffer_store: Entity<BufferStore>,
354        cx: &mut Context<Self>,
355    ) -> Task<anyhow::Result<Entity<Buffer>>> {
356        if let Some(buffer) = self.commit_message_buffer.clone() {
357            return Task::ready(Ok(buffer));
358        }
359
360        if let GitRepo::Remote {
361            project_id,
362            client,
363            worktree_id,
364            work_directory_id,
365        } = self.git_repo.clone()
366        {
367            let client = client.clone();
368            cx.spawn(|repository, mut cx| async move {
369                let request = client.request(proto::OpenCommitMessageBuffer {
370                    project_id: project_id.0,
371                    worktree_id: worktree_id.to_proto(),
372                    work_directory_id: work_directory_id.to_proto(),
373                });
374                let response = request.await.context("requesting to open commit buffer")?;
375                let buffer_id = BufferId::new(response.buffer_id)?;
376                let buffer = buffer_store
377                    .update(&mut cx, |buffer_store, cx| {
378                        buffer_store.wait_for_remote_buffer(buffer_id, cx)
379                    })?
380                    .await?;
381                if let Some(language_registry) = languages {
382                    let git_commit_language =
383                        language_registry.language_for_name("Git Commit").await?;
384                    buffer.update(&mut cx, |buffer, cx| {
385                        buffer.set_language(Some(git_commit_language), cx);
386                    })?;
387                }
388                repository.update(&mut cx, |repository, _| {
389                    repository.commit_message_buffer = Some(buffer.clone());
390                })?;
391                Ok(buffer)
392            })
393        } else {
394            self.open_local_commit_buffer(languages, buffer_store, cx)
395        }
396    }
397
398    fn open_local_commit_buffer(
399        &mut self,
400        language_registry: Option<Arc<LanguageRegistry>>,
401        buffer_store: Entity<BufferStore>,
402        cx: &mut Context<Self>,
403    ) -> Task<anyhow::Result<Entity<Buffer>>> {
404        cx.spawn(|repository, mut cx| async move {
405            let buffer = buffer_store
406                .update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
407                .await?;
408
409            if let Some(language_registry) = language_registry {
410                let git_commit_language = language_registry.language_for_name("Git Commit").await?;
411                buffer.update(&mut cx, |buffer, cx| {
412                    buffer.set_language(Some(git_commit_language), cx);
413                })?;
414            }
415
416            repository.update(&mut cx, |repository, _| {
417                repository.commit_message_buffer = Some(buffer.clone());
418            })?;
419            Ok(buffer)
420        })
421    }
422
423    pub fn stage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
424        let (result_tx, result_rx) = futures::channel::oneshot::channel();
425        if entries.is_empty() {
426            result_tx.send(Ok(())).ok();
427            return result_rx;
428        }
429        self.update_sender
430            .unbounded_send((Message::Stage(self.git_repo.clone(), entries), result_tx))
431            .ok();
432        result_rx
433    }
434
435    pub fn unstage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
436        let (result_tx, result_rx) = futures::channel::oneshot::channel();
437        if entries.is_empty() {
438            result_tx.send(Ok(())).ok();
439            return result_rx;
440        }
441        self.update_sender
442            .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), result_tx))
443            .ok();
444        result_rx
445    }
446
447    pub fn stage_all(&self) -> oneshot::Receiver<anyhow::Result<()>> {
448        let to_stage = self
449            .repository_entry
450            .status()
451            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
452            .map(|entry| entry.repo_path.clone())
453            .collect();
454        self.stage_entries(to_stage)
455    }
456
457    pub fn unstage_all(&self) -> oneshot::Receiver<anyhow::Result<()>> {
458        let to_unstage = self
459            .repository_entry
460            .status()
461            .filter(|entry| entry.status.is_staged().unwrap_or(true))
462            .map(|entry| entry.repo_path.clone())
463            .collect();
464        self.unstage_entries(to_unstage)
465    }
466
467    /// Get a count of all entries in the active repository, including
468    /// untracked files.
469    pub fn entry_count(&self) -> usize {
470        self.repository_entry.status_len()
471    }
472
473    fn have_changes(&self) -> bool {
474        self.repository_entry.status_summary() != GitSummary::UNCHANGED
475    }
476
477    fn have_staged_changes(&self) -> bool {
478        self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
479    }
480
481    pub fn can_commit(&self, commit_all: bool) -> bool {
482        return self.have_changes() && (commit_all || self.have_staged_changes());
483    }
484
485    pub fn commit(
486        &self,
487        message: SharedString,
488        name_and_email: Option<(SharedString, SharedString)>,
489    ) -> oneshot::Receiver<anyhow::Result<()>> {
490        let (result_tx, result_rx) = futures::channel::oneshot::channel();
491        self.update_sender
492            .unbounded_send((
493                Message::Commit {
494                    git_repo: self.git_repo.clone(),
495                    message,
496                    name_and_email,
497                },
498                result_tx,
499            ))
500            .ok();
501        result_rx
502    }
503}