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, PathBuf};
 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 branch(&self) -> Option<Arc<str>> {
303        self.repository_entry.branch()
304    }
305
306    pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
307        maybe!({
308            let project_path = self.repo_path_to_project_path(&"".into())?;
309            let worktree_name = project
310                .worktree_for_id(project_path.worktree_id, cx)?
311                .read(cx)
312                .root_name();
313
314            let mut path = PathBuf::new();
315            path = path.join(worktree_name);
316            path = path.join(project_path.path);
317            Some(path.to_string_lossy().to_string())
318        })
319        .unwrap_or_else(|| self.repository_entry.work_directory.display_name())
320        .into()
321    }
322
323    pub fn activate(&self, cx: &mut Context<Self>) {
324        let Some(git_state) = self.git_state.upgrade() else {
325            return;
326        };
327        let entity = cx.entity();
328        git_state.update(cx, |git_state, cx| {
329            let Some(index) = git_state
330                .repositories
331                .iter()
332                .position(|handle| *handle == entity)
333            else {
334                return;
335            };
336            git_state.active_index = Some(index);
337            cx.emit(GitEvent::ActiveRepositoryChanged);
338        });
339    }
340
341    pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
342        self.repository_entry.status()
343    }
344
345    pub fn has_conflict(&self, path: &RepoPath) -> bool {
346        self.repository_entry
347            .current_merge_conflicts
348            .contains(&path)
349    }
350
351    pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option<ProjectPath> {
352        let path = self.repository_entry.unrelativize(path)?;
353        Some((self.worktree_id, path).into())
354    }
355
356    pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
357        self.worktree_id_path_to_repo_path(path.worktree_id, &path.path)
358    }
359
360    pub fn worktree_id_path_to_repo_path(
361        &self,
362        worktree_id: WorktreeId,
363        path: &Path,
364    ) -> Option<RepoPath> {
365        if worktree_id != self.worktree_id {
366            return None;
367        }
368        self.repository_entry.relativize(path).log_err()
369    }
370
371    pub fn open_commit_buffer(
372        &mut self,
373        languages: Option<Arc<LanguageRegistry>>,
374        buffer_store: Entity<BufferStore>,
375        cx: &mut Context<Self>,
376    ) -> Task<anyhow::Result<Entity<Buffer>>> {
377        if let Some(buffer) = self.commit_message_buffer.clone() {
378            return Task::ready(Ok(buffer));
379        }
380
381        if let GitRepo::Remote {
382            project_id,
383            client,
384            worktree_id,
385            work_directory_id,
386        } = self.git_repo.clone()
387        {
388            let client = client.clone();
389            cx.spawn(|repository, mut cx| async move {
390                let request = client.request(proto::OpenCommitMessageBuffer {
391                    project_id: project_id.0,
392                    worktree_id: worktree_id.to_proto(),
393                    work_directory_id: work_directory_id.to_proto(),
394                });
395                let response = request.await.context("requesting to open commit buffer")?;
396                let buffer_id = BufferId::new(response.buffer_id)?;
397                let buffer = buffer_store
398                    .update(&mut cx, |buffer_store, cx| {
399                        buffer_store.wait_for_remote_buffer(buffer_id, cx)
400                    })?
401                    .await?;
402                if let Some(language_registry) = languages {
403                    let git_commit_language =
404                        language_registry.language_for_name("Git Commit").await?;
405                    buffer.update(&mut cx, |buffer, cx| {
406                        buffer.set_language(Some(git_commit_language), cx);
407                    })?;
408                }
409                repository.update(&mut cx, |repository, _| {
410                    repository.commit_message_buffer = Some(buffer.clone());
411                })?;
412                Ok(buffer)
413            })
414        } else {
415            self.open_local_commit_buffer(languages, buffer_store, cx)
416        }
417    }
418
419    fn open_local_commit_buffer(
420        &mut self,
421        language_registry: Option<Arc<LanguageRegistry>>,
422        buffer_store: Entity<BufferStore>,
423        cx: &mut Context<Self>,
424    ) -> Task<anyhow::Result<Entity<Buffer>>> {
425        cx.spawn(|repository, mut cx| async move {
426            let buffer = buffer_store
427                .update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
428                .await?;
429
430            if let Some(language_registry) = language_registry {
431                let git_commit_language = language_registry.language_for_name("Git Commit").await?;
432                buffer.update(&mut cx, |buffer, cx| {
433                    buffer.set_language(Some(git_commit_language), cx);
434                })?;
435            }
436
437            repository.update(&mut cx, |repository, _| {
438                repository.commit_message_buffer = Some(buffer.clone());
439            })?;
440            Ok(buffer)
441        })
442    }
443
444    pub fn stage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
445        let (result_tx, result_rx) = futures::channel::oneshot::channel();
446        if entries.is_empty() {
447            result_tx.send(Ok(())).ok();
448            return result_rx;
449        }
450        self.update_sender
451            .unbounded_send((Message::Stage(self.git_repo.clone(), entries), result_tx))
452            .ok();
453        result_rx
454    }
455
456    pub fn unstage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
457        let (result_tx, result_rx) = futures::channel::oneshot::channel();
458        if entries.is_empty() {
459            result_tx.send(Ok(())).ok();
460            return result_rx;
461        }
462        self.update_sender
463            .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), result_tx))
464            .ok();
465        result_rx
466    }
467
468    pub fn stage_all(&self) -> oneshot::Receiver<anyhow::Result<()>> {
469        let to_stage = self
470            .repository_entry
471            .status()
472            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
473            .map(|entry| entry.repo_path.clone())
474            .collect();
475        self.stage_entries(to_stage)
476    }
477
478    pub fn unstage_all(&self) -> oneshot::Receiver<anyhow::Result<()>> {
479        let to_unstage = self
480            .repository_entry
481            .status()
482            .filter(|entry| entry.status.is_staged().unwrap_or(true))
483            .map(|entry| entry.repo_path.clone())
484            .collect();
485        self.unstage_entries(to_unstage)
486    }
487
488    /// Get a count of all entries in the active repository, including
489    /// untracked files.
490    pub fn entry_count(&self) -> usize {
491        self.repository_entry.status_len()
492    }
493
494    fn have_changes(&self) -> bool {
495        self.repository_entry.status_summary() != GitSummary::UNCHANGED
496    }
497
498    fn have_staged_changes(&self) -> bool {
499        self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
500    }
501
502    pub fn can_commit(&self, commit_all: bool) -> bool {
503        return self.have_changes() && (commit_all || self.have_staged_changes());
504    }
505
506    pub fn commit(
507        &self,
508        message: SharedString,
509        name_and_email: Option<(SharedString, SharedString)>,
510    ) -> oneshot::Receiver<anyhow::Result<()>> {
511        let (result_tx, result_rx) = futures::channel::oneshot::channel();
512        self.update_sender
513            .unbounded_send((
514                Message::Commit {
515                    git_repo: self.git_repo.clone(),
516                    message,
517                    name_and_email,
518                },
519                result_tx,
520            ))
521            .ok();
522        result_rx
523    }
524}