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