git.rs

  1use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
  2use crate::{Project, ProjectPath};
  3use anyhow::anyhow;
  4use futures::channel::mpsc;
  5use futures::{SinkExt as _, StreamExt as _};
  6use git::{
  7    repository::{GitRepository, RepoPath},
  8    status::{GitSummary, TrackedSummary},
  9};
 10use gpui::{
 11    App, AppContext as _, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity,
 12};
 13use language::{Buffer, LanguageRegistry};
 14use settings::WorktreeId;
 15use std::sync::Arc;
 16use text::Rope;
 17use util::maybe;
 18use worktree::{RepositoryEntry, StatusEntry};
 19
 20pub struct GitState {
 21    repositories: Vec<RepositoryHandle>,
 22    active_index: Option<usize>,
 23    update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
 24    languages: Arc<LanguageRegistry>,
 25    _subscription: Subscription,
 26}
 27
 28#[derive(Clone)]
 29pub struct RepositoryHandle {
 30    git_state: WeakEntity<GitState>,
 31    worktree_id: WorktreeId,
 32    repository_entry: RepositoryEntry,
 33    git_repo: Option<Arc<dyn GitRepository>>,
 34    commit_message: Entity<Buffer>,
 35    update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
 36}
 37
 38impl PartialEq<Self> for RepositoryHandle {
 39    fn eq(&self, other: &Self) -> bool {
 40        self.worktree_id == other.worktree_id
 41            && self.repository_entry.work_directory_id()
 42                == other.repository_entry.work_directory_id()
 43    }
 44}
 45
 46impl Eq for RepositoryHandle {}
 47
 48impl PartialEq<RepositoryEntry> for RepositoryHandle {
 49    fn eq(&self, other: &RepositoryEntry) -> bool {
 50        self.repository_entry.work_directory_id() == other.work_directory_id()
 51    }
 52}
 53
 54enum Message {
 55    StageAndCommit(Arc<dyn GitRepository>, Rope, Vec<RepoPath>),
 56    Commit(Arc<dyn GitRepository>, Rope),
 57    Stage(Arc<dyn GitRepository>, Vec<RepoPath>),
 58    Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
 59}
 60
 61pub enum Event {
 62    RepositoriesUpdated,
 63}
 64
 65impl EventEmitter<Event> for GitState {}
 66
 67impl GitState {
 68    pub fn new(
 69        worktree_store: &Entity<WorktreeStore>,
 70        languages: Arc<LanguageRegistry>,
 71        cx: &mut Context<'_, Self>,
 72    ) -> Self {
 73        let (update_sender, mut update_receiver) =
 74            mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
 75        cx.spawn(|_, cx| async move {
 76            while let Some((msg, mut err_sender)) = update_receiver.next().await {
 77                let result = cx
 78                    .background_executor()
 79                    .spawn(async move {
 80                        match msg {
 81                            Message::StageAndCommit(repo, message, paths) => {
 82                                repo.stage_paths(&paths)?;
 83                                repo.commit(&message.to_string())?;
 84                                Ok(())
 85                            }
 86                            Message::Stage(repo, paths) => repo.stage_paths(&paths),
 87                            Message::Unstage(repo, paths) => repo.unstage_paths(&paths),
 88                            Message::Commit(repo, message) => repo.commit(&message.to_string()),
 89                        }
 90                    })
 91                    .await;
 92                if let Err(e) = result {
 93                    err_sender.send(e).await.ok();
 94                }
 95            }
 96        })
 97        .detach();
 98
 99        let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
100
101        GitState {
102            languages,
103            repositories: Vec::new(),
104            active_index: None,
105            update_sender,
106            _subscription,
107        }
108    }
109
110    pub fn active_repository(&self) -> Option<RepositoryHandle> {
111        self.active_index
112            .map(|index| self.repositories[index].clone())
113    }
114
115    fn on_worktree_store_event(
116        &mut self,
117        worktree_store: Entity<WorktreeStore>,
118        _event: &WorktreeStoreEvent,
119        cx: &mut Context<'_, Self>,
120    ) {
121        // TODO inspect the event
122
123        let mut new_repositories = Vec::new();
124        let mut new_active_index = None;
125        let this = cx.weak_entity();
126
127        worktree_store.update(cx, |worktree_store, cx| {
128            for worktree in worktree_store.worktrees() {
129                worktree.update(cx, |worktree, cx| {
130                    let snapshot = worktree.snapshot();
131                    for repo in snapshot.repositories().iter() {
132                        let git_repo = worktree
133                            .as_local()
134                            .and_then(|local_worktree| local_worktree.get_local_repo(repo))
135                            .map(|local_repo| local_repo.repo().clone());
136                        let existing = self
137                            .repositories
138                            .iter()
139                            .enumerate()
140                            .find(|(_, existing_handle)| existing_handle == &repo);
141                        let handle = if let Some((index, handle)) = existing {
142                            if self.active_index == Some(index) {
143                                new_active_index = Some(new_repositories.len());
144                            }
145                            // Update the statuses but keep everything else.
146                            let mut existing_handle = handle.clone();
147                            existing_handle.repository_entry = repo.clone();
148                            existing_handle
149                        } else {
150                            let commit_message = cx.new(|cx| Buffer::local("", cx));
151                            cx.spawn({
152                                let commit_message = commit_message.downgrade();
153                                let languages = self.languages.clone();
154                                |_, mut cx| async move {
155                                    let markdown = languages.language_for_name("Markdown").await?;
156                                    commit_message.update(&mut cx, |commit_message, cx| {
157                                        commit_message.set_language(Some(markdown), cx);
158                                    })?;
159                                    anyhow::Ok(())
160                                }
161                            })
162                            .detach_and_log_err(cx);
163                            RepositoryHandle {
164                                git_state: this.clone(),
165                                worktree_id: worktree.id(),
166                                repository_entry: repo.clone(),
167                                git_repo,
168                                commit_message,
169                                update_sender: self.update_sender.clone(),
170                            }
171                        };
172                        new_repositories.push(handle);
173                    }
174                })
175            }
176        });
177
178        if new_active_index == None && new_repositories.len() > 0 {
179            new_active_index = Some(0);
180        }
181
182        self.repositories = new_repositories;
183        self.active_index = new_active_index;
184
185        cx.emit(Event::RepositoriesUpdated);
186    }
187
188    pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
189        self.repositories.clone()
190    }
191}
192
193impl RepositoryHandle {
194    pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
195        maybe!({
196            let path = self.unrelativize(&"".into())?;
197            Some(
198                project
199                    .absolute_path(&path, cx)?
200                    .file_name()?
201                    .to_string_lossy()
202                    .to_string()
203                    .into(),
204            )
205        })
206        .unwrap_or("".into())
207    }
208
209    pub fn activate(&self, cx: &mut App) {
210        let Some(git_state) = self.git_state.upgrade() else {
211            return;
212        };
213        git_state.update(cx, |git_state, cx| {
214            let Some((index, _)) = git_state
215                .repositories
216                .iter()
217                .enumerate()
218                .find(|(_, handle)| handle == &self)
219            else {
220                return;
221            };
222            git_state.active_index = Some(index);
223            cx.emit(Event::RepositoriesUpdated);
224        });
225    }
226
227    pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
228        self.repository_entry.status()
229    }
230
231    pub fn unrelativize(&self, path: &RepoPath) -> Option<ProjectPath> {
232        let path = self.repository_entry.unrelativize(path)?;
233        Some((self.worktree_id, path).into())
234    }
235
236    pub fn commit_message(&self) -> Entity<Buffer> {
237        self.commit_message.clone()
238    }
239
240    pub fn stage_entries(
241        &self,
242        entries: Vec<RepoPath>,
243        err_sender: mpsc::Sender<anyhow::Error>,
244    ) -> anyhow::Result<()> {
245        if entries.is_empty() {
246            return Ok(());
247        }
248        let Some(git_repo) = self.git_repo.clone() else {
249            return Ok(());
250        };
251        self.update_sender
252            .unbounded_send((Message::Stage(git_repo, entries), err_sender))
253            .map_err(|_| anyhow!("Failed to submit stage operation"))?;
254        Ok(())
255    }
256
257    pub fn unstage_entries(
258        &self,
259        entries: Vec<RepoPath>,
260        err_sender: mpsc::Sender<anyhow::Error>,
261    ) -> anyhow::Result<()> {
262        if entries.is_empty() {
263            return Ok(());
264        }
265        let Some(git_repo) = self.git_repo.clone() else {
266            return Ok(());
267        };
268        self.update_sender
269            .unbounded_send((Message::Unstage(git_repo, entries), err_sender))
270            .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
271        Ok(())
272    }
273
274    pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
275        let to_stage = self
276            .repository_entry
277            .status()
278            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
279            .map(|entry| entry.repo_path.clone())
280            .collect();
281        self.stage_entries(to_stage, err_sender)?;
282        Ok(())
283    }
284
285    pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
286        let to_unstage = self
287            .repository_entry
288            .status()
289            .filter(|entry| entry.status.is_staged().unwrap_or(true))
290            .map(|entry| entry.repo_path.clone())
291            .collect();
292        self.unstage_entries(to_unstage, err_sender)?;
293        Ok(())
294    }
295
296    /// Get a count of all entries in the active repository, including
297    /// untracked files.
298    pub fn entry_count(&self) -> usize {
299        self.repository_entry.status_len()
300    }
301
302    fn have_changes(&self) -> bool {
303        self.repository_entry.status_summary() != GitSummary::UNCHANGED
304    }
305
306    fn have_staged_changes(&self) -> bool {
307        self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
308    }
309
310    pub fn can_commit(&self, commit_all: bool, cx: &App) -> bool {
311        return self
312            .commit_message
313            .read(cx)
314            .chars()
315            .any(|c| !c.is_ascii_whitespace())
316            && self.have_changes()
317            && (commit_all || self.have_staged_changes());
318    }
319
320    pub fn commit(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
321        let Some(git_repo) = self.git_repo.clone() else {
322            return;
323        };
324        let message = self.commit_message.read(cx).as_rope().clone();
325        let result = self
326            .update_sender
327            .unbounded_send((Message::Commit(git_repo, message), err_sender.clone()));
328        if result.is_err() {
329            cx.spawn(|_| async move {
330                err_sender
331                    .send(anyhow!("Failed to submit commit operation"))
332                    .await
333                    .ok();
334            })
335            .detach();
336            return;
337        }
338        self.commit_message.update(cx, |commit_message, cx| {
339            commit_message.set_text("", cx);
340        });
341    }
342
343    pub fn commit_all(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
344        let Some(git_repo) = self.git_repo.clone() else {
345            return;
346        };
347        let to_stage = self
348            .repository_entry
349            .status()
350            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
351            .map(|entry| entry.repo_path.clone())
352            .collect::<Vec<_>>();
353        let message = self.commit_message.read(cx).as_rope().clone();
354        let result = self.update_sender.unbounded_send((
355            Message::StageAndCommit(git_repo, message, to_stage),
356            err_sender.clone(),
357        ));
358        if result.is_err() {
359            cx.spawn(|_| async move {
360                err_sender
361                    .send(anyhow!("Failed to submit commit all operation"))
362                    .await
363                    .ok();
364            })
365            .detach();
366            return;
367        }
368        self.commit_message.update(cx, |commit_message, cx| {
369            commit_message.set_text("", cx);
370        });
371    }
372}