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}