git.rs

  1use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
  2use crate::{Project, ProjectPath};
  3use anyhow::{anyhow, Context as _};
  4use client::ProjectId;
  5use futures::channel::{mpsc, oneshot};
  6use futures::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, oneshot::Sender<anyhow::Result<()>>)>,
 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, oneshot::Sender<anyhow::Result<()>>)>,
 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    Commit {
 65        git_repo: GitRepo,
 66        name_and_email: Option<(SharedString, SharedString)>,
 67    },
 68    Stage(GitRepo, Vec<RepoPath>),
 69    Unstage(GitRepo, Vec<RepoPath>),
 70}
 71
 72pub enum Event {
 73    RepositoriesUpdated,
 74}
 75
 76impl EventEmitter<Event> for GitState {}
 77
 78impl GitState {
 79    pub fn new(
 80        worktree_store: &Entity<WorktreeStore>,
 81        client: Option<AnyProtoClient>,
 82        project_id: Option<ProjectId>,
 83        cx: &mut Context<'_, Self>,
 84    ) -> Self {
 85        let update_sender = Self::spawn_git_worker(cx);
 86        let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
 87
 88        GitState {
 89            project_id,
 90            client,
 91            repositories: Vec::new(),
 92            active_index: None,
 93            update_sender,
 94            _subscription,
 95        }
 96    }
 97
 98    pub fn active_repository(&self) -> Option<RepositoryHandle> {
 99        self.active_index
100            .map(|index| self.repositories[index].clone())
101    }
102
103    fn on_worktree_store_event(
104        &mut self,
105        worktree_store: Entity<WorktreeStore>,
106        _event: &WorktreeStoreEvent,
107        cx: &mut Context<'_, Self>,
108    ) {
109        // TODO inspect the event
110
111        let mut new_repositories = Vec::new();
112        let mut new_active_index = None;
113        let this = cx.weak_entity();
114        let client = self.client.clone();
115        let project_id = self.project_id;
116
117        worktree_store.update(cx, |worktree_store, cx| {
118            for worktree in worktree_store.worktrees() {
119                worktree.update(cx, |worktree, _| {
120                    let snapshot = worktree.snapshot();
121                    for repo in snapshot.repositories().iter() {
122                        let git_repo = worktree
123                            .as_local()
124                            .and_then(|local_worktree| local_worktree.get_local_repo(repo))
125                            .map(|local_repo| local_repo.repo().clone())
126                            .map(GitRepo::Local)
127                            .or_else(|| {
128                                let client = client.clone()?;
129                                let project_id = project_id?;
130                                Some(GitRepo::Remote {
131                                    project_id,
132                                    client,
133                                    worktree_id: worktree.id(),
134                                    work_directory_id: repo.work_directory_id(),
135                                })
136                            });
137                        let Some(git_repo) = git_repo else {
138                            continue;
139                        };
140                        let existing = self
141                            .repositories
142                            .iter()
143                            .enumerate()
144                            .find(|(_, existing_handle)| existing_handle == &repo);
145                        let handle = if let Some((index, handle)) = existing {
146                            if self.active_index == Some(index) {
147                                new_active_index = Some(new_repositories.len());
148                            }
149                            // Update the statuses but keep everything else.
150                            let mut existing_handle = handle.clone();
151                            existing_handle.repository_entry = repo.clone();
152                            existing_handle
153                        } else {
154                            RepositoryHandle {
155                                git_state: this.clone(),
156                                worktree_id: worktree.id(),
157                                repository_entry: repo.clone(),
158                                git_repo,
159                                update_sender: self.update_sender.clone(),
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        cx.emit(Event::RepositoriesUpdated);
176    }
177
178    pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
179        self.repositories.clone()
180    }
181
182    fn spawn_git_worker(
183        cx: &mut Context<'_, GitState>,
184    ) -> mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)> {
185        let (update_sender, mut update_receiver) =
186            mpsc::unbounded::<(Message, oneshot::Sender<anyhow::Result<()>>)>();
187        cx.spawn(|_, cx| async move {
188            while let Some((msg, respond)) = update_receiver.next().await {
189                let result = cx
190                    .background_executor()
191                    .spawn(Self::process_git_msg(msg))
192                    .await;
193                respond.send(result).ok();
194            }
195        })
196        .detach();
197        update_sender
198    }
199
200    async fn process_git_msg(msg: Message) -> Result<(), anyhow::Error> {
201        match msg {
202            Message::Stage(repo, paths) => {
203                match repo {
204                    GitRepo::Local(repo) => repo.stage_paths(&paths)?,
205                    GitRepo::Remote {
206                        project_id,
207                        client,
208                        worktree_id,
209                        work_directory_id,
210                    } => {
211                        client
212                            .request(proto::Stage {
213                                project_id: project_id.0,
214                                worktree_id: worktree_id.to_proto(),
215                                work_directory_id: work_directory_id.to_proto(),
216                                paths: paths
217                                    .into_iter()
218                                    .map(|repo_path| repo_path.to_proto())
219                                    .collect(),
220                            })
221                            .await
222                            .context("sending stage request")?;
223                    }
224                }
225                Ok(())
226            }
227            Message::Unstage(repo, paths) => {
228                match repo {
229                    GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
230                    GitRepo::Remote {
231                        project_id,
232                        client,
233                        worktree_id,
234                        work_directory_id,
235                    } => {
236                        client
237                            .request(proto::Unstage {
238                                project_id: project_id.0,
239                                worktree_id: worktree_id.to_proto(),
240                                work_directory_id: work_directory_id.to_proto(),
241                                paths: paths
242                                    .into_iter()
243                                    .map(|repo_path| repo_path.to_proto())
244                                    .collect(),
245                            })
246                            .await
247                            .context("sending unstage request")?;
248                    }
249                }
250                Ok(())
251            }
252            Message::Commit {
253                git_repo,
254                name_and_email,
255            } => {
256                match git_repo {
257                    GitRepo::Local(repo) => repo.commit(
258                        name_and_email
259                            .as_ref()
260                            .map(|(name, email)| (name.as_ref(), email.as_ref())),
261                    )?,
262                    GitRepo::Remote {
263                        project_id,
264                        client,
265                        worktree_id,
266                        work_directory_id,
267                    } => {
268                        let (name, email) = name_and_email.unzip();
269                        client
270                            .request(proto::Commit {
271                                project_id: project_id.0,
272                                worktree_id: worktree_id.to_proto(),
273                                work_directory_id: work_directory_id.to_proto(),
274                                name: name.map(String::from),
275                                email: email.map(String::from),
276                            })
277                            .await
278                            .context("sending commit request")?;
279                    }
280                }
281                Ok(())
282            }
283        }
284    }
285}
286
287impl RepositoryHandle {
288    pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
289        maybe!({
290            let path = self.repo_path_to_project_path(&"".into())?;
291            Some(
292                project
293                    .absolute_path(&path, cx)?
294                    .file_name()?
295                    .to_string_lossy()
296                    .to_string()
297                    .into(),
298            )
299        })
300        .unwrap_or("".into())
301    }
302
303    pub fn activate(&self, cx: &mut App) {
304        let Some(git_state) = self.git_state.upgrade() else {
305            return;
306        };
307        git_state.update(cx, |git_state, cx| {
308            let Some((index, _)) = git_state
309                .repositories
310                .iter()
311                .enumerate()
312                .find(|(_, handle)| handle == &self)
313            else {
314                return;
315            };
316            git_state.active_index = Some(index);
317            cx.emit(Event::RepositoriesUpdated);
318        });
319    }
320
321    pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
322        self.repository_entry.status()
323    }
324
325    pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option<ProjectPath> {
326        let path = self.repository_entry.unrelativize(path)?;
327        Some((self.worktree_id, path).into())
328    }
329
330    pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
331        if path.worktree_id != self.worktree_id {
332            return None;
333        }
334        self.repository_entry.relativize(&path.path).log_err()
335    }
336
337    pub async fn stage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
338        if entries.is_empty() {
339            return Ok(());
340        }
341        let (result_tx, result_rx) = futures::channel::oneshot::channel();
342        self.update_sender
343            .unbounded_send((Message::Stage(self.git_repo.clone(), entries), result_tx))
344            .map_err(|_| anyhow!("Failed to submit stage operation"))?;
345
346        result_rx.await?
347    }
348
349    pub async fn unstage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
350        if entries.is_empty() {
351            return Ok(());
352        }
353        let (result_tx, result_rx) = futures::channel::oneshot::channel();
354        self.update_sender
355            .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), result_tx))
356            .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
357        result_rx.await?
358    }
359
360    pub async fn stage_all(&self) -> anyhow::Result<()> {
361        let to_stage = self
362            .repository_entry
363            .status()
364            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
365            .map(|entry| entry.repo_path.clone())
366            .collect();
367        self.stage_entries(to_stage).await
368    }
369
370    pub async fn unstage_all(&self) -> anyhow::Result<()> {
371        let to_unstage = self
372            .repository_entry
373            .status()
374            .filter(|entry| entry.status.is_staged().unwrap_or(true))
375            .map(|entry| entry.repo_path.clone())
376            .collect();
377        self.unstage_entries(to_unstage).await
378    }
379
380    /// Get a count of all entries in the active repository, including
381    /// untracked files.
382    pub fn entry_count(&self) -> usize {
383        self.repository_entry.status_len()
384    }
385
386    fn have_changes(&self) -> bool {
387        self.repository_entry.status_summary() != GitSummary::UNCHANGED
388    }
389
390    fn have_staged_changes(&self) -> bool {
391        self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
392    }
393
394    pub fn can_commit(&self, commit_all: bool) -> bool {
395        return self.have_changes() && (commit_all || self.have_staged_changes());
396    }
397
398    pub async fn commit(
399        &self,
400        name_and_email: Option<(SharedString, SharedString)>,
401    ) -> anyhow::Result<()> {
402        let (result_tx, result_rx) = futures::channel::oneshot::channel();
403        self.update_sender.unbounded_send((
404            Message::Commit {
405                git_repo: self.git_repo.clone(),
406                name_and_email,
407            },
408            result_tx,
409        ))?;
410        result_rx.await?
411    }
412}