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 GitEvent {
 73    ActiveRepositoryChanged,
 74    FileSystemUpdated,
 75    GitStateUpdated,
 76}
 77
 78impl EventEmitter<GitEvent> for GitState {}
 79
 80impl GitState {
 81    pub fn new(
 82        worktree_store: &Entity<WorktreeStore>,
 83        client: Option<AnyProtoClient>,
 84        project_id: Option<ProjectId>,
 85        cx: &mut Context<'_, Self>,
 86    ) -> Self {
 87        let update_sender = Self::spawn_git_worker(cx);
 88        let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
 89
 90        GitState {
 91            project_id,
 92            client,
 93            repositories: Vec::new(),
 94            active_index: None,
 95            update_sender,
 96            _subscription,
 97        }
 98    }
 99
100    pub fn active_repository(&self) -> Option<RepositoryHandle> {
101        self.active_index
102            .map(|index| self.repositories[index].clone())
103    }
104
105    fn on_worktree_store_event(
106        &mut self,
107        worktree_store: Entity<WorktreeStore>,
108        event: &WorktreeStoreEvent,
109        cx: &mut Context<'_, Self>,
110    ) {
111        // TODO inspect the event
112
113        let mut new_repositories = Vec::new();
114        let mut new_active_index = None;
115        let this = cx.weak_entity();
116        let client = self.client.clone();
117        let project_id = self.project_id;
118
119        worktree_store.update(cx, |worktree_store, cx| {
120            for worktree in worktree_store.worktrees() {
121                worktree.update(cx, |worktree, _| {
122                    let snapshot = worktree.snapshot();
123                    for repo in snapshot.repositories().iter() {
124                        let git_repo = worktree
125                            .as_local()
126                            .and_then(|local_worktree| local_worktree.get_local_repo(repo))
127                            .map(|local_repo| local_repo.repo().clone())
128                            .map(GitRepo::Local)
129                            .or_else(|| {
130                                let client = client.clone()?;
131                                let project_id = project_id?;
132                                Some(GitRepo::Remote {
133                                    project_id,
134                                    client,
135                                    worktree_id: worktree.id(),
136                                    work_directory_id: repo.work_directory_id(),
137                                })
138                            });
139                        let Some(git_repo) = git_repo else {
140                            continue;
141                        };
142                        let existing = self
143                            .repositories
144                            .iter()
145                            .enumerate()
146                            .find(|(_, existing_handle)| existing_handle == &repo);
147                        let handle = if let Some((index, handle)) = existing {
148                            if self.active_index == Some(index) {
149                                new_active_index = Some(new_repositories.len());
150                            }
151                            // Update the statuses but keep everything else.
152                            let mut existing_handle = handle.clone();
153                            existing_handle.repository_entry = repo.clone();
154                            existing_handle
155                        } else {
156                            RepositoryHandle {
157                                git_state: this.clone(),
158                                worktree_id: worktree.id(),
159                                repository_entry: repo.clone(),
160                                git_repo,
161                                update_sender: self.update_sender.clone(),
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<RepositoryHandle> {
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.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.to_proto())
253                                    .collect(),
254                            })
255                            .await
256                            .context("sending unstage request")?;
257                    }
258                }
259                Ok(())
260            }
261            Message::Commit {
262                git_repo,
263                name_and_email,
264            } => {
265                match git_repo {
266                    GitRepo::Local(repo) => repo.commit(
267                        name_and_email
268                            .as_ref()
269                            .map(|(name, email)| (name.as_ref(), email.as_ref())),
270                    )?,
271                    GitRepo::Remote {
272                        project_id,
273                        client,
274                        worktree_id,
275                        work_directory_id,
276                    } => {
277                        let (name, email) = name_and_email.unzip();
278                        client
279                            .request(proto::Commit {
280                                project_id: project_id.0,
281                                worktree_id: worktree_id.to_proto(),
282                                work_directory_id: work_directory_id.to_proto(),
283                                name: name.map(String::from),
284                                email: email.map(String::from),
285                            })
286                            .await
287                            .context("sending commit request")?;
288                    }
289                }
290                Ok(())
291            }
292        }
293    }
294}
295
296impl RepositoryHandle {
297    pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
298        maybe!({
299            let path = self.repo_path_to_project_path(&"".into())?;
300            Some(
301                project
302                    .absolute_path(&path, cx)?
303                    .file_name()?
304                    .to_string_lossy()
305                    .to_string()
306                    .into(),
307            )
308        })
309        .unwrap_or("".into())
310    }
311
312    pub fn activate(&self, cx: &mut App) {
313        let Some(git_state) = self.git_state.upgrade() else {
314            return;
315        };
316        git_state.update(cx, |git_state, cx| {
317            let Some((index, _)) = git_state
318                .repositories
319                .iter()
320                .enumerate()
321                .find(|(_, handle)| handle == &self)
322            else {
323                return;
324            };
325            git_state.active_index = Some(index);
326            cx.emit(GitEvent::ActiveRepositoryChanged);
327        });
328    }
329
330    pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
331        self.repository_entry.status()
332    }
333
334    pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option<ProjectPath> {
335        let path = self.repository_entry.unrelativize(path)?;
336        Some((self.worktree_id, path).into())
337    }
338
339    pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
340        if path.worktree_id != self.worktree_id {
341            return None;
342        }
343        self.repository_entry.relativize(&path.path).log_err()
344    }
345
346    pub async fn stage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
347        if entries.is_empty() {
348            return Ok(());
349        }
350        let (result_tx, result_rx) = futures::channel::oneshot::channel();
351        self.update_sender
352            .unbounded_send((Message::Stage(self.git_repo.clone(), entries), result_tx))
353            .map_err(|_| anyhow!("Failed to submit stage operation"))?;
354
355        result_rx.await?
356    }
357
358    pub async fn unstage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
359        if entries.is_empty() {
360            return Ok(());
361        }
362        let (result_tx, result_rx) = futures::channel::oneshot::channel();
363        self.update_sender
364            .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), result_tx))
365            .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
366        result_rx.await?
367    }
368
369    pub async fn stage_all(&self) -> anyhow::Result<()> {
370        let to_stage = self
371            .repository_entry
372            .status()
373            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
374            .map(|entry| entry.repo_path.clone())
375            .collect();
376        self.stage_entries(to_stage).await
377    }
378
379    pub async fn unstage_all(&self) -> anyhow::Result<()> {
380        let to_unstage = self
381            .repository_entry
382            .status()
383            .filter(|entry| entry.status.is_staged().unwrap_or(true))
384            .map(|entry| entry.repo_path.clone())
385            .collect();
386        self.unstage_entries(to_unstage).await
387    }
388
389    /// Get a count of all entries in the active repository, including
390    /// untracked files.
391    pub fn entry_count(&self) -> usize {
392        self.repository_entry.status_len()
393    }
394
395    fn have_changes(&self) -> bool {
396        self.repository_entry.status_summary() != GitSummary::UNCHANGED
397    }
398
399    fn have_staged_changes(&self) -> bool {
400        self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
401    }
402
403    pub fn can_commit(&self, commit_all: bool) -> bool {
404        return self.have_changes() && (commit_all || self.have_staged_changes());
405    }
406
407    pub async fn commit(
408        &self,
409        name_and_email: Option<(SharedString, SharedString)>,
410    ) -> anyhow::Result<()> {
411        let (result_tx, result_rx) = futures::channel::oneshot::channel();
412        self.update_sender.unbounded_send((
413            Message::Commit {
414                git_repo: self.git_repo.clone(),
415                name_and_email,
416            },
417            result_tx,
418        ))?;
419        result_rx.await?
420    }
421}