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, ResultExt};
 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.repo_path_to_project_path(&"".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 repo_path_to_project_path(&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 project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
376        if path.worktree_id != self.worktree_id {
377            return None;
378        }
379        self.repository_entry.relativize(&path.path).log_err()
380    }
381
382    pub fn stage_entries(
383        &self,
384        entries: Vec<RepoPath>,
385        err_sender: mpsc::Sender<anyhow::Error>,
386    ) -> anyhow::Result<()> {
387        if entries.is_empty() {
388            return Ok(());
389        }
390        self.update_sender
391            .unbounded_send((Message::Stage(self.git_repo.clone(), entries), err_sender))
392            .map_err(|_| anyhow!("Failed to submit stage operation"))?;
393        Ok(())
394    }
395
396    pub fn unstage_entries(
397        &self,
398        entries: Vec<RepoPath>,
399        err_sender: mpsc::Sender<anyhow::Error>,
400    ) -> anyhow::Result<()> {
401        if entries.is_empty() {
402            return Ok(());
403        }
404        self.update_sender
405            .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), err_sender))
406            .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
407        Ok(())
408    }
409
410    pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
411        let to_stage = self
412            .repository_entry
413            .status()
414            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
415            .map(|entry| entry.repo_path.clone())
416            .collect();
417        self.stage_entries(to_stage, err_sender)?;
418        Ok(())
419    }
420
421    pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
422        let to_unstage = self
423            .repository_entry
424            .status()
425            .filter(|entry| entry.status.is_staged().unwrap_or(true))
426            .map(|entry| entry.repo_path.clone())
427            .collect();
428        self.unstage_entries(to_unstage, err_sender)?;
429        Ok(())
430    }
431
432    /// Get a count of all entries in the active repository, including
433    /// untracked files.
434    pub fn entry_count(&self) -> usize {
435        self.repository_entry.status_len()
436    }
437
438    fn have_changes(&self) -> bool {
439        self.repository_entry.status_summary() != GitSummary::UNCHANGED
440    }
441
442    fn have_staged_changes(&self) -> bool {
443        self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
444    }
445
446    pub fn can_commit(&self, commit_all: bool) -> bool {
447        return self.have_changes() && (commit_all || self.have_staged_changes());
448    }
449
450    pub fn commit(
451        &self,
452        name_and_email: Option<(SharedString, SharedString)>,
453        mut err_sender: mpsc::Sender<anyhow::Error>,
454        cx: &mut App,
455    ) -> anyhow::Result<()> {
456        let result = self.update_sender.unbounded_send((
457            Message::Commit {
458                git_repo: self.git_repo.clone(),
459                name_and_email,
460            },
461            err_sender.clone(),
462        ));
463        if result.is_err() {
464            cx.spawn(|_| async move {
465                err_sender
466                    .send(anyhow!("Failed to submit commit operation"))
467                    .await
468                    .ok();
469            })
470            .detach();
471            anyhow::bail!("Failed to submit commit operation");
472        } else {
473            Ok(())
474        }
475    }
476
477    pub fn commit_all(
478        &self,
479        name_and_email: Option<(SharedString, SharedString)>,
480        mut err_sender: mpsc::Sender<anyhow::Error>,
481        cx: &mut App,
482    ) -> anyhow::Result<()> {
483        let to_stage = self
484            .repository_entry
485            .status()
486            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
487            .map(|entry| entry.repo_path.clone())
488            .collect();
489        let result = self.update_sender.unbounded_send((
490            Message::StageAndCommit {
491                git_repo: self.git_repo.clone(),
492                paths: to_stage,
493                name_and_email,
494            },
495            err_sender.clone(),
496        ));
497        if result.is_err() {
498            cx.spawn(|_| async move {
499                err_sender
500                    .send(anyhow!("Failed to submit commit all operation"))
501                    .await
502                    .ok();
503            })
504            .detach();
505            anyhow::bail!("Failed to submit commit all operation");
506        } else {
507            Ok(())
508        }
509    }
510}