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