git.rs

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