git.rs

  1use std::sync::Arc;
  2
  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::{AppContext, SharedString};
 11use settings::WorktreeId;
 12use worktree::RepositoryEntry;
 13
 14pub struct GitState {
 15    /// The current commit message being composed.
 16    pub commit_message: SharedString,
 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>, SharedString, Vec<RepoPath>),
 27    Commit(Arc<dyn GitRepository>, SharedString),
 28    Stage(Arc<dyn GitRepository>, Vec<RepoPath>),
 29    Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
 30}
 31
 32impl GitState {
 33    pub fn new(cx: &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)?;
 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),
 50                        }
 51                    })
 52                    .await;
 53                if let Err(e) = result {
 54                    err_sender.send(e).await.ok();
 55                }
 56            }
 57        })
 58        .detach();
 59        GitState {
 60            commit_message: SharedString::default(),
 61            active_repository: None,
 62            update_sender,
 63        }
 64    }
 65
 66    pub fn activate_repository(
 67        &mut self,
 68        worktree_id: WorktreeId,
 69        active_repository: RepositoryEntry,
 70        git_repo: Arc<dyn GitRepository>,
 71    ) {
 72        self.active_repository = Some((worktree_id, active_repository, git_repo));
 73    }
 74
 75    pub fn active_repository(
 76        &self,
 77    ) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
 78        self.active_repository.as_ref()
 79    }
 80
 81    pub fn stage_entries(
 82        &self,
 83        entries: Vec<RepoPath>,
 84        err_sender: mpsc::Sender<anyhow::Error>,
 85    ) -> anyhow::Result<()> {
 86        if entries.is_empty() {
 87            return Ok(());
 88        }
 89        let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
 90            return Err(anyhow!("No active repository"));
 91        };
 92        self.update_sender
 93            .unbounded_send((Message::Stage(git_repo.clone(), entries), err_sender))
 94            .map_err(|_| anyhow!("Failed to submit stage operation"))?;
 95        Ok(())
 96    }
 97
 98    pub fn unstage_entries(
 99        &self,
100        entries: Vec<RepoPath>,
101        err_sender: mpsc::Sender<anyhow::Error>,
102    ) -> anyhow::Result<()> {
103        if entries.is_empty() {
104            return Ok(());
105        }
106        let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
107            return Err(anyhow!("No active repository"));
108        };
109        self.update_sender
110            .unbounded_send((Message::Unstage(git_repo.clone(), entries), err_sender))
111            .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
112        Ok(())
113    }
114
115    pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
116        let Some((_, entry, _)) = self.active_repository.as_ref() else {
117            return Err(anyhow!("No active repository"));
118        };
119        let to_stage = entry
120            .status()
121            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
122            .map(|entry| entry.repo_path.clone())
123            .collect();
124        self.stage_entries(to_stage, err_sender)?;
125        Ok(())
126    }
127
128    pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
129        let Some((_, entry, _)) = self.active_repository.as_ref() else {
130            return Err(anyhow!("No active repository"));
131        };
132        let to_unstage = entry
133            .status()
134            .filter(|entry| entry.status.is_staged().unwrap_or(true))
135            .map(|entry| entry.repo_path.clone())
136            .collect();
137        self.unstage_entries(to_unstage, err_sender)?;
138        Ok(())
139    }
140
141    /// Get a count of all entries in the active repository, including
142    /// untracked files.
143    pub fn entry_count(&self) -> usize {
144        self.active_repository
145            .as_ref()
146            .map_or(0, |(_, entry, _)| entry.status_len())
147    }
148
149    fn have_changes(&self) -> bool {
150        let Some((_, entry, _)) = self.active_repository.as_ref() else {
151            return false;
152        };
153        entry.status_summary() != GitSummary::UNCHANGED
154    }
155
156    fn have_staged_changes(&self) -> bool {
157        let Some((_, entry, _)) = self.active_repository.as_ref() else {
158            return false;
159        };
160        entry.status_summary().index != TrackedSummary::UNCHANGED
161    }
162
163    pub fn can_commit(&self, commit_all: bool) -> bool {
164        return !self.commit_message.trim().is_empty()
165            && self.have_changes()
166            && (commit_all || self.have_staged_changes());
167    }
168
169    pub fn commit(&mut self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
170        if !self.can_commit(false) {
171            return Err(anyhow!("Unable to commit"));
172        }
173        let Some((_, _, git_repo)) = self.active_repository() else {
174            return Err(anyhow!("No active repository"));
175        };
176        let git_repo = git_repo.clone();
177        let message = std::mem::take(&mut self.commit_message);
178        self.update_sender
179            .unbounded_send((Message::Commit(git_repo, message), err_sender))
180            .map_err(|_| anyhow!("Failed to submit commit operation"))?;
181        Ok(())
182    }
183
184    pub fn commit_all(&mut self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
185        if !self.can_commit(true) {
186            return Err(anyhow!("Unable to commit"));
187        }
188        let Some((_, entry, git_repo)) = self.active_repository.as_ref() else {
189            return Err(anyhow!("No active repository"));
190        };
191        let to_stage = entry
192            .status()
193            .filter(|entry| !entry.status.is_staged().unwrap_or(false))
194            .map(|entry| entry.repo_path.clone())
195            .collect::<Vec<_>>();
196        let message = std::mem::take(&mut self.commit_message);
197        self.update_sender
198            .unbounded_send((
199                Message::StageAndCommit(git_repo.clone(), message, to_stage),
200                err_sender,
201            ))
202            .map_err(|_| anyhow!("Failed to submit commit operation"))?;
203        Ok(())
204    }
205}