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: 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![],
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                    let Some(local) = worktree.as_local() else {
132                        return;
133                    };
134                    for repo in snapshot.repositories().iter() {
135                        let Some(local_repo) = local.get_local_repo(repo) else {
136                            continue;
137                        };
138                        let existing = self
139                            .repositories
140                            .iter()
141                            .enumerate()
142                            .find(|(_, existing_handle)| existing_handle == &repo);
143                        let handle = if let Some((index, handle)) = existing {
144                            if self.active_index == Some(index) {
145                                new_active_index = Some(new_repositories.len());
146                            }
147                            // Update the statuses but keep everything else.
148                            let mut existing_handle = handle.clone();
149                            existing_handle.repository_entry = repo.clone();
150                            existing_handle
151                        } else {
152                            let commit_message = cx.new(|cx| Buffer::local("", cx));
153                            cx.spawn({
154                                let commit_message = commit_message.downgrade();
155                                let languages = self.languages.clone();
156                                |_, mut cx| async move {
157                                    let markdown = languages.language_for_name("Markdown").await?;
158                                    commit_message.update(&mut cx, |commit_message, cx| {
159                                        commit_message.set_language(Some(markdown), cx);
160                                    })?;
161                                    anyhow::Ok(())
162                                }
163                            })
164                            .detach_and_log_err(cx);
165                            RepositoryHandle {
166                                git_state: this.clone(),
167                                worktree_id: worktree.id(),
168                                repository_entry: repo.clone(),
169                                git_repo: local_repo.repo().clone(),
170                                commit_message,
171                                update_sender: self.update_sender.clone(),
172                            }
173                        };
174                        new_repositories.push(handle);
175                    }
176                })
177            }
178        });
179
180        if new_active_index == None && new_repositories.len() > 0 {
181            new_active_index = Some(0);
182        }
183
184        self.repositories = new_repositories;
185        self.active_index = new_active_index;
186
187        cx.emit(Event::RepositoriesUpdated);
188    }
189
190    pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
191        self.repositories.clone()
192    }
193}
194
195impl RepositoryHandle {
196    pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
197        maybe!({
198            let path = self.unrelativize(&"".into())?;
199            Some(
200                project
201                    .absolute_path(&path, cx)?
202                    .file_name()?
203                    .to_string_lossy()
204                    .to_string()
205                    .into(),
206            )
207        })
208        .unwrap_or("".into())
209    }
210
211    pub fn activate(&self, cx: &mut App) {
212        let Some(git_state) = self.git_state.upgrade() else {
213            return;
214        };
215        git_state.update(cx, |git_state, cx| {
216            let Some((index, _)) = git_state
217                .repositories
218                .iter()
219                .enumerate()
220                .find(|(_, handle)| handle == &self)
221            else {
222                return;
223            };
224            git_state.active_index = Some(index);
225            cx.emit(Event::RepositoriesUpdated);
226        });
227    }
228
229    pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
230        self.repository_entry.status()
231    }
232
233    pub fn unrelativize(&self, path: &RepoPath) -> Option<ProjectPath> {
234        let path = self.repository_entry.unrelativize(path)?;
235        Some((self.worktree_id, path).into())
236    }
237
238    pub fn commit_message(&self) -> Entity<Buffer> {
239        self.commit_message.clone()
240    }
241
242    pub fn stage_entries(
243        &self,
244        entries: Vec<RepoPath>,
245        err_sender: mpsc::Sender<anyhow::Error>,
246    ) -> anyhow::Result<()> {
247        if entries.is_empty() {
248            return Ok(());
249        }
250        self.update_sender
251            .unbounded_send((Message::Stage(self.git_repo.clone(), entries), err_sender))
252            .map_err(|_| anyhow!("Failed to submit stage operation"))?;
253        Ok(())
254    }
255
256    pub fn unstage_entries(
257        &self,
258        entries: Vec<RepoPath>,
259        err_sender: mpsc::Sender<anyhow::Error>,
260    ) -> anyhow::Result<()> {
261        if entries.is_empty() {
262            return Ok(());
263        }
264        self.update_sender
265            .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), err_sender))
266            .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
267        Ok(())
268    }
269
270    pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
271        let to_stage = self
272            .repository_entry
273            .status()
274            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
275            .map(|entry| entry.repo_path.clone())
276            .collect();
277        self.stage_entries(to_stage, err_sender)?;
278        Ok(())
279    }
280
281    pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
282        let to_unstage = self
283            .repository_entry
284            .status()
285            .filter(|entry| entry.status.is_staged().unwrap_or(true))
286            .map(|entry| entry.repo_path.clone())
287            .collect();
288        self.unstage_entries(to_unstage, err_sender)?;
289        Ok(())
290    }
291
292    /// Get a count of all entries in the active repository, including
293    /// untracked files.
294    pub fn entry_count(&self) -> usize {
295        self.repository_entry.status_len()
296    }
297
298    fn have_changes(&self) -> bool {
299        self.repository_entry.status_summary() != GitSummary::UNCHANGED
300    }
301
302    fn have_staged_changes(&self) -> bool {
303        self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
304    }
305
306    pub fn can_commit(&self, commit_all: bool, cx: &App) -> bool {
307        return self
308            .commit_message
309            .read(cx)
310            .chars()
311            .any(|c| !c.is_ascii_whitespace())
312            && self.have_changes()
313            && (commit_all || self.have_staged_changes());
314    }
315
316    pub fn commit(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
317        let message = self.commit_message.read(cx).as_rope().clone();
318        let result = self.update_sender.unbounded_send((
319            Message::Commit(self.git_repo.clone(), message),
320            err_sender.clone(),
321        ));
322        if result.is_err() {
323            cx.spawn(|_| async move {
324                err_sender
325                    .send(anyhow!("Failed to submit commit operation"))
326                    .await
327                    .ok();
328            })
329            .detach();
330            return;
331        }
332        self.commit_message.update(cx, |commit_message, cx| {
333            commit_message.set_text("", cx);
334        });
335    }
336
337    pub fn commit_all(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
338        let to_stage = self
339            .repository_entry
340            .status()
341            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
342            .map(|entry| entry.repo_path.clone())
343            .collect::<Vec<_>>();
344        let message = self.commit_message.read(cx).as_rope().clone();
345        let result = self.update_sender.unbounded_send((
346            Message::StageAndCommit(self.git_repo.clone(), message, to_stage),
347            err_sender.clone(),
348        ));
349        if result.is_err() {
350            cx.spawn(|_| async move {
351                err_sender
352                    .send(anyhow!("Failed to submit commit all operation"))
353                    .await
354                    .ok();
355            })
356            .detach();
357            return;
358        }
359        self.commit_message.update(cx, |commit_message, cx| {
360            commit_message.set_text("", cx);
361        });
362    }
363}