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