git.rs

  1use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
  2use crate::{Project, ProjectPath};
  3use anyhow::{anyhow, Context as _};
  4use client::ProjectId;
  5use futures::channel::mpsc;
  6use futures::{SinkExt as _, StreamExt as _};
  7use git::{
  8    repository::{GitRepository, RepoPath},
  9    status::{GitSummary, TrackedSummary},
 10};
 11use gpui::{
 12    App, AppContext as _, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity,
 13};
 14use language::{Buffer, LanguageRegistry};
 15use rpc::{proto, AnyProtoClient};
 16use settings::WorktreeId;
 17use std::sync::Arc;
 18use text::Rope;
 19use util::maybe;
 20use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
 21
 22pub struct GitState {
 23    project_id: Option<ProjectId>,
 24    client: Option<AnyProtoClient>,
 25    repositories: Vec<RepositoryHandle>,
 26    active_index: Option<usize>,
 27    update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
 28    languages: Arc<LanguageRegistry>,
 29    _subscription: Subscription,
 30}
 31
 32#[derive(Clone)]
 33pub struct RepositoryHandle {
 34    git_state: WeakEntity<GitState>,
 35    pub worktree_id: WorktreeId,
 36    pub repository_entry: RepositoryEntry,
 37    git_repo: Option<GitRepo>,
 38    commit_message: Entity<Buffer>,
 39    update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
 40}
 41
 42#[derive(Clone)]
 43enum GitRepo {
 44    Local(Arc<dyn GitRepository>),
 45    Remote {
 46        project_id: ProjectId,
 47        client: AnyProtoClient,
 48        worktree_id: WorktreeId,
 49        work_directory_id: ProjectEntryId,
 50    },
 51}
 52
 53impl PartialEq<Self> for RepositoryHandle {
 54    fn eq(&self, other: &Self) -> bool {
 55        self.worktree_id == other.worktree_id
 56            && self.repository_entry.work_directory_id()
 57                == other.repository_entry.work_directory_id()
 58    }
 59}
 60
 61impl Eq for RepositoryHandle {}
 62
 63impl PartialEq<RepositoryEntry> for RepositoryHandle {
 64    fn eq(&self, other: &RepositoryEntry) -> bool {
 65        self.repository_entry.work_directory_id() == other.work_directory_id()
 66    }
 67}
 68
 69enum Message {
 70    StageAndCommit(GitRepo, Rope, Vec<RepoPath>),
 71    Commit(GitRepo, Rope),
 72    Stage(GitRepo, Vec<RepoPath>),
 73    Unstage(GitRepo, Vec<RepoPath>),
 74}
 75
 76pub enum Event {
 77    RepositoriesUpdated,
 78}
 79
 80impl EventEmitter<Event> for GitState {}
 81
 82impl GitState {
 83    pub fn new(
 84        worktree_store: &Entity<WorktreeStore>,
 85        languages: Arc<LanguageRegistry>,
 86        client: Option<AnyProtoClient>,
 87        project_id: Option<ProjectId>,
 88        cx: &mut Context<'_, Self>,
 89    ) -> Self {
 90        let (update_sender, mut update_receiver) =
 91            mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
 92        cx.spawn(|_, cx| async move {
 93            while let Some((msg, mut err_sender)) = update_receiver.next().await {
 94                let result = cx
 95                    .background_executor()
 96                    .spawn(async move {
 97                        match msg {
 98                            Message::StageAndCommit(repo, message, paths) => {
 99                                match repo {
100                                    GitRepo::Local(repo) => {
101                                        repo.stage_paths(&paths)?;
102                                        repo.commit(&message.to_string())?;
103                                    }
104                                    GitRepo::Remote {
105                                        project_id,
106                                        client,
107                                        worktree_id,
108                                        work_directory_id,
109                                    } => {
110                                        client
111                                            .request(proto::Stage {
112                                                project_id: project_id.0,
113                                                worktree_id: worktree_id.to_proto(),
114                                                work_directory_id: work_directory_id.to_proto(),
115                                                paths: paths
116                                                    .into_iter()
117                                                    .map(|repo_path| repo_path.to_proto())
118                                                    .collect(),
119                                            })
120                                            .await
121                                            .context("sending stage request")?;
122                                        client
123                                            .request(proto::Commit {
124                                                project_id: project_id.0,
125                                                worktree_id: worktree_id.to_proto(),
126                                                work_directory_id: work_directory_id.to_proto(),
127                                                message: message.to_string(),
128                                            })
129                                            .await
130                                            .context("sending commit request")?;
131                                    }
132                                }
133
134                                Ok(())
135                            }
136                            Message::Stage(repo, paths) => {
137                                match repo {
138                                    GitRepo::Local(repo) => repo.stage_paths(&paths)?,
139                                    GitRepo::Remote {
140                                        project_id,
141                                        client,
142                                        worktree_id,
143                                        work_directory_id,
144                                    } => {
145                                        client
146                                            .request(proto::Stage {
147                                                project_id: project_id.0,
148                                                worktree_id: worktree_id.to_proto(),
149                                                work_directory_id: work_directory_id.to_proto(),
150                                                paths: paths
151                                                    .into_iter()
152                                                    .map(|repo_path| repo_path.to_proto())
153                                                    .collect(),
154                                            })
155                                            .await
156                                            .context("sending stage request")?;
157                                    }
158                                }
159                                Ok(())
160                            }
161                            Message::Unstage(repo, paths) => {
162                                match repo {
163                                    GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
164                                    GitRepo::Remote {
165                                        project_id,
166                                        client,
167                                        worktree_id,
168                                        work_directory_id,
169                                    } => {
170                                        client
171                                            .request(proto::Unstage {
172                                                project_id: project_id.0,
173                                                worktree_id: worktree_id.to_proto(),
174                                                work_directory_id: work_directory_id.to_proto(),
175                                                paths: paths
176                                                    .into_iter()
177                                                    .map(|repo_path| repo_path.to_proto())
178                                                    .collect(),
179                                            })
180                                            .await
181                                            .context("sending unstage request")?;
182                                    }
183                                }
184                                Ok(())
185                            }
186                            Message::Commit(repo, message) => {
187                                match repo {
188                                    GitRepo::Local(repo) => repo.commit(&message.to_string())?,
189                                    GitRepo::Remote {
190                                        project_id,
191                                        client,
192                                        worktree_id,
193                                        work_directory_id,
194                                    } => {
195                                        client
196                                            .request(proto::Commit {
197                                                project_id: project_id.0,
198                                                worktree_id: worktree_id.to_proto(),
199                                                work_directory_id: work_directory_id.to_proto(),
200                                                // TODO implement collaborative commit message buffer instead and use it
201                                                // If it works, remove `commit_with_message` method.
202                                                message: message.to_string(),
203                                            })
204                                            .await
205                                            .context("sending commit request")?;
206                                    }
207                                }
208                                Ok(())
209                            }
210                        }
211                    })
212                    .await;
213                if let Err(e) = result {
214                    err_sender.send(e).await.ok();
215                }
216            }
217        })
218        .detach();
219
220        let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
221
222        GitState {
223            project_id,
224            languages,
225            client,
226            repositories: Vec::new(),
227            active_index: None,
228            update_sender,
229            _subscription,
230        }
231    }
232
233    pub fn active_repository(&self) -> Option<RepositoryHandle> {
234        self.active_index
235            .map(|index| self.repositories[index].clone())
236    }
237
238    fn on_worktree_store_event(
239        &mut self,
240        worktree_store: Entity<WorktreeStore>,
241        _event: &WorktreeStoreEvent,
242        cx: &mut Context<'_, Self>,
243    ) {
244        // TODO inspect the event
245
246        let mut new_repositories = Vec::new();
247        let mut new_active_index = None;
248        let this = cx.weak_entity();
249        let client = self.client.clone();
250        let project_id = self.project_id;
251
252        worktree_store.update(cx, |worktree_store, cx| {
253            for worktree in worktree_store.worktrees() {
254                worktree.update(cx, |worktree, cx| {
255                    let snapshot = worktree.snapshot();
256                    for repo in snapshot.repositories().iter() {
257                        let git_repo = worktree
258                            .as_local()
259                            .and_then(|local_worktree| local_worktree.get_local_repo(repo))
260                            .map(|local_repo| local_repo.repo().clone())
261                            .map(GitRepo::Local)
262                            .or_else(|| {
263                                let client = client.clone()?;
264                                let project_id = project_id?;
265                                Some(GitRepo::Remote {
266                                    project_id,
267                                    client,
268                                    worktree_id: worktree.id(),
269                                    work_directory_id: repo.work_directory_id(),
270                                })
271                            });
272                        let existing = self
273                            .repositories
274                            .iter()
275                            .enumerate()
276                            .find(|(_, existing_handle)| existing_handle == &repo);
277                        let handle = if let Some((index, handle)) = existing {
278                            if self.active_index == Some(index) {
279                                new_active_index = Some(new_repositories.len());
280                            }
281                            // Update the statuses but keep everything else.
282                            let mut existing_handle = handle.clone();
283                            existing_handle.repository_entry = repo.clone();
284                            existing_handle
285                        } else {
286                            let commit_message = cx.new(|cx| Buffer::local("", cx));
287                            cx.spawn({
288                                let commit_message = commit_message.downgrade();
289                                let languages = self.languages.clone();
290                                |_, mut cx| async move {
291                                    let markdown = languages.language_for_name("Markdown").await?;
292                                    commit_message.update(&mut cx, |commit_message, cx| {
293                                        commit_message.set_language(Some(markdown), cx);
294                                    })?;
295                                    anyhow::Ok(())
296                                }
297                            })
298                            .detach_and_log_err(cx);
299                            RepositoryHandle {
300                                git_state: this.clone(),
301                                worktree_id: worktree.id(),
302                                repository_entry: repo.clone(),
303                                git_repo,
304                                commit_message,
305                                update_sender: self.update_sender.clone(),
306                            }
307                        };
308                        new_repositories.push(handle);
309                    }
310                })
311            }
312        });
313
314        if new_active_index == None && new_repositories.len() > 0 {
315            new_active_index = Some(0);
316        }
317
318        self.repositories = new_repositories;
319        self.active_index = new_active_index;
320
321        cx.emit(Event::RepositoriesUpdated);
322    }
323
324    pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
325        self.repositories.clone()
326    }
327}
328
329impl RepositoryHandle {
330    pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
331        maybe!({
332            let path = self.unrelativize(&"".into())?;
333            Some(
334                project
335                    .absolute_path(&path, cx)?
336                    .file_name()?
337                    .to_string_lossy()
338                    .to_string()
339                    .into(),
340            )
341        })
342        .unwrap_or("".into())
343    }
344
345    pub fn activate(&self, cx: &mut App) {
346        let Some(git_state) = self.git_state.upgrade() else {
347            return;
348        };
349        git_state.update(cx, |git_state, cx| {
350            let Some((index, _)) = git_state
351                .repositories
352                .iter()
353                .enumerate()
354                .find(|(_, handle)| handle == &self)
355            else {
356                return;
357            };
358            git_state.active_index = Some(index);
359            cx.emit(Event::RepositoriesUpdated);
360        });
361    }
362
363    pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
364        self.repository_entry.status()
365    }
366
367    pub fn unrelativize(&self, path: &RepoPath) -> Option<ProjectPath> {
368        let path = self.repository_entry.unrelativize(path)?;
369        Some((self.worktree_id, path).into())
370    }
371
372    pub fn commit_message(&self) -> Entity<Buffer> {
373        self.commit_message.clone()
374    }
375
376    pub fn stage_entries(
377        &self,
378        entries: Vec<RepoPath>,
379        err_sender: mpsc::Sender<anyhow::Error>,
380    ) -> anyhow::Result<()> {
381        if entries.is_empty() {
382            return Ok(());
383        }
384        let Some(git_repo) = self.git_repo.clone() else {
385            return Ok(());
386        };
387        self.update_sender
388            .unbounded_send((Message::Stage(git_repo, entries), err_sender))
389            .map_err(|_| anyhow!("Failed to submit stage operation"))?;
390        Ok(())
391    }
392
393    pub fn unstage_entries(
394        &self,
395        entries: Vec<RepoPath>,
396        err_sender: mpsc::Sender<anyhow::Error>,
397    ) -> anyhow::Result<()> {
398        if entries.is_empty() {
399            return Ok(());
400        }
401        let Some(git_repo) = self.git_repo.clone() else {
402            return Ok(());
403        };
404        self.update_sender
405            .unbounded_send((Message::Unstage(git_repo, entries), err_sender))
406            .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
407        Ok(())
408    }
409
410    pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
411        let to_stage = self
412            .repository_entry
413            .status()
414            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
415            .map(|entry| entry.repo_path.clone())
416            .collect();
417        self.stage_entries(to_stage, err_sender)?;
418        Ok(())
419    }
420
421    pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
422        let to_unstage = self
423            .repository_entry
424            .status()
425            .filter(|entry| entry.status.is_staged().unwrap_or(true))
426            .map(|entry| entry.repo_path.clone())
427            .collect();
428        self.unstage_entries(to_unstage, err_sender)?;
429        Ok(())
430    }
431
432    /// Get a count of all entries in the active repository, including
433    /// untracked files.
434    pub fn entry_count(&self) -> usize {
435        self.repository_entry.status_len()
436    }
437
438    fn have_changes(&self) -> bool {
439        self.repository_entry.status_summary() != GitSummary::UNCHANGED
440    }
441
442    fn have_staged_changes(&self) -> bool {
443        self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
444    }
445
446    pub fn can_commit(&self, commit_all: bool, cx: &App) -> bool {
447        return self
448            .commit_message
449            .read(cx)
450            .chars()
451            .any(|c| !c.is_ascii_whitespace())
452            && self.have_changes()
453            && (commit_all || self.have_staged_changes());
454    }
455
456    pub fn commit(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
457        let Some(git_repo) = self.git_repo.clone() else {
458            return;
459        };
460        let message = self.commit_message.read(cx).as_rope().clone();
461        let result = self
462            .update_sender
463            .unbounded_send((Message::Commit(git_repo, message), err_sender.clone()));
464        if result.is_err() {
465            cx.spawn(|_| async move {
466                err_sender
467                    .send(anyhow!("Failed to submit commit operation"))
468                    .await
469                    .ok();
470            })
471            .detach();
472            return;
473        }
474        self.commit_message.update(cx, |commit_message, cx| {
475            commit_message.set_text("", cx);
476        });
477    }
478
479    pub fn commit_with_message(
480        &self,
481        message: String,
482        err_sender: mpsc::Sender<anyhow::Error>,
483    ) -> anyhow::Result<()> {
484        let Some(git_repo) = self.git_repo.clone() else {
485            return Ok(());
486        };
487        let result = self
488            .update_sender
489            .unbounded_send((Message::Commit(git_repo, message.into()), err_sender));
490        anyhow::ensure!(result.is_ok(), "Failed to submit commit operation");
491        Ok(())
492    }
493
494    pub fn commit_all(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
495        let Some(git_repo) = self.git_repo.clone() else {
496            return;
497        };
498        let to_stage = self
499            .repository_entry
500            .status()
501            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
502            .map(|entry| entry.repo_path.clone())
503            .collect::<Vec<_>>();
504        let message = self.commit_message.read(cx).as_rope().clone();
505        let result = self.update_sender.unbounded_send((
506            Message::StageAndCommit(git_repo, message, to_stage),
507            err_sender.clone(),
508        ));
509        if result.is_err() {
510            cx.spawn(|_| async move {
511                err_sender
512                    .send(anyhow!("Failed to submit commit all operation"))
513                    .await
514                    .ok();
515            })
516            .detach();
517            return;
518        }
519        self.commit_message.update(cx, |commit_message, cx| {
520            commit_message.set_text("", cx);
521        });
522    }
523}