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