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