git.rs

  1use anyhow::{anyhow, Context as _};
  2use futures::channel::mpsc;
  3use futures::{SinkExt as _, StreamExt as _};
  4use git::{
  5    repository::{GitRepository, RepoPath},
  6    status::{GitSummary, TrackedSummary},
  7};
  8use gpui::{AppContext, Context as _, Model};
  9use language::{Buffer, LanguageRegistry};
 10use settings::WorktreeId;
 11use std::sync::Arc;
 12use text::Rope;
 13use worktree::RepositoryEntry;
 14
 15pub struct GitState {
 16    pub commit_message: Model<Buffer>,
 17
 18    /// When a git repository is selected, this is used to track which repository's changes
 19    /// are currently being viewed or modified in the UI.
 20    pub active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
 21
 22    update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
 23}
 24
 25enum Message {
 26    StageAndCommit(Arc<dyn GitRepository>, Rope, Vec<RepoPath>),
 27    Commit(Arc<dyn GitRepository>, Rope),
 28    Stage(Arc<dyn GitRepository>, Vec<RepoPath>),
 29    Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
 30}
 31
 32impl GitState {
 33    pub fn new(languages: Arc<LanguageRegistry>, cx: &mut AppContext) -> Self {
 34        let (update_sender, mut update_receiver) =
 35            mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
 36        cx.spawn(|cx| async move {
 37            while let Some((msg, mut err_sender)) = update_receiver.next().await {
 38                let result = cx
 39                    .background_executor()
 40                    .spawn(async move {
 41                        match msg {
 42                            Message::StageAndCommit(repo, message, paths) => {
 43                                repo.stage_paths(&paths)?;
 44                                repo.commit(&message.to_string())?;
 45                                Ok(())
 46                            }
 47                            Message::Stage(repo, paths) => repo.stage_paths(&paths),
 48                            Message::Unstage(repo, paths) => repo.unstage_paths(&paths),
 49                            Message::Commit(repo, message) => repo.commit(&message.to_string()),
 50                        }
 51                    })
 52                    .await;
 53                if let Err(e) = result {
 54                    err_sender.send(e).await.ok();
 55                }
 56            }
 57        })
 58        .detach();
 59
 60        let commit_message = cx.new_model(|cx| Buffer::local("", cx));
 61        let markdown = languages.language_for_name("Markdown");
 62        cx.spawn({
 63            let commit_message = commit_message.clone();
 64            |mut cx| async move {
 65                let markdown = markdown.await.context("failed to load Markdown language")?;
 66                commit_message.update(&mut cx, |commit_message, cx| {
 67                    commit_message.set_language(Some(markdown), cx)
 68                })
 69            }
 70        })
 71        .detach_and_log_err(cx);
 72
 73        GitState {
 74            commit_message,
 75            active_repository: None,
 76            update_sender,
 77        }
 78    }
 79
 80    pub fn activate_repository(
 81        &mut self,
 82        worktree_id: WorktreeId,
 83        active_repository: RepositoryEntry,
 84        git_repo: Arc<dyn GitRepository>,
 85    ) {
 86        self.active_repository = Some((worktree_id, active_repository, git_repo));
 87    }
 88
 89    pub fn active_repository(
 90        &self,
 91    ) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
 92        self.active_repository.as_ref()
 93    }
 94
 95    pub fn stage_entries(
 96        &self,
 97        entries: Vec<RepoPath>,
 98        err_sender: mpsc::Sender<anyhow::Error>,
 99    ) -> anyhow::Result<()> {
100        if entries.is_empty() {
101            return Ok(());
102        }
103        let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
104            return Err(anyhow!("No active repository"));
105        };
106        self.update_sender
107            .unbounded_send((Message::Stage(git_repo.clone(), entries), err_sender))
108            .map_err(|_| anyhow!("Failed to submit stage operation"))?;
109        Ok(())
110    }
111
112    pub fn unstage_entries(
113        &self,
114        entries: Vec<RepoPath>,
115        err_sender: mpsc::Sender<anyhow::Error>,
116    ) -> anyhow::Result<()> {
117        if entries.is_empty() {
118            return Ok(());
119        }
120        let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
121            return Err(anyhow!("No active repository"));
122        };
123        self.update_sender
124            .unbounded_send((Message::Unstage(git_repo.clone(), entries), err_sender))
125            .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
126        Ok(())
127    }
128
129    pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
130        let Some((_, entry, _)) = self.active_repository.as_ref() else {
131            return Err(anyhow!("No active repository"));
132        };
133        let to_stage = entry
134            .status()
135            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
136            .map(|entry| entry.repo_path.clone())
137            .collect();
138        self.stage_entries(to_stage, err_sender)?;
139        Ok(())
140    }
141
142    pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
143        let Some((_, entry, _)) = self.active_repository.as_ref() else {
144            return Err(anyhow!("No active repository"));
145        };
146        let to_unstage = entry
147            .status()
148            .filter(|entry| entry.status.is_staged().unwrap_or(true))
149            .map(|entry| entry.repo_path.clone())
150            .collect();
151        self.unstage_entries(to_unstage, err_sender)?;
152        Ok(())
153    }
154
155    /// Get a count of all entries in the active repository, including
156    /// untracked files.
157    pub fn entry_count(&self) -> usize {
158        self.active_repository
159            .as_ref()
160            .map_or(0, |(_, entry, _)| entry.status_len())
161    }
162
163    fn have_changes(&self) -> bool {
164        let Some((_, entry, _)) = self.active_repository.as_ref() else {
165            return false;
166        };
167        entry.status_summary() != GitSummary::UNCHANGED
168    }
169
170    fn have_staged_changes(&self) -> bool {
171        let Some((_, entry, _)) = self.active_repository.as_ref() else {
172            return false;
173        };
174        entry.status_summary().index != TrackedSummary::UNCHANGED
175    }
176
177    pub fn can_commit(&self, commit_all: bool, cx: &AppContext) -> bool {
178        return self
179            .commit_message
180            .read(cx)
181            .chars()
182            .any(|c| !c.is_ascii_whitespace())
183            && self.have_changes()
184            && (commit_all || self.have_staged_changes());
185    }
186
187    pub fn commit(
188        &mut self,
189        err_sender: mpsc::Sender<anyhow::Error>,
190        cx: &AppContext,
191    ) -> anyhow::Result<()> {
192        if !self.can_commit(false, cx) {
193            return Err(anyhow!("Unable to commit"));
194        }
195        let Some((_, _, git_repo)) = self.active_repository() else {
196            return Err(anyhow!("No active repository"));
197        };
198        let git_repo = git_repo.clone();
199        let message = self.commit_message.read(cx).as_rope().clone();
200        self.update_sender
201            .unbounded_send((Message::Commit(git_repo, message), err_sender))
202            .map_err(|_| anyhow!("Failed to submit commit operation"))?;
203        Ok(())
204    }
205
206    pub fn commit_all(
207        &mut self,
208        err_sender: mpsc::Sender<anyhow::Error>,
209        cx: &AppContext,
210    ) -> anyhow::Result<()> {
211        if !self.can_commit(true, cx) {
212            return Err(anyhow!("Unable to commit"));
213        }
214        let Some((_, entry, git_repo)) = self.active_repository.as_ref() else {
215            return Err(anyhow!("No active repository"));
216        };
217        let to_stage = entry
218            .status()
219            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
220            .map(|entry| entry.repo_path.clone())
221            .collect::<Vec<_>>();
222        let message = self.commit_message.read(cx).as_rope().clone();
223        self.update_sender
224            .unbounded_send((
225                Message::StageAndCommit(git_repo.clone(), message, to_stage),
226                err_sender,
227            ))
228            .map_err(|_| anyhow!("Failed to submit commit operation"))?;
229        Ok(())
230    }
231}