1use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
2use crate::{Project, ProjectPath};
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::{
11 App, AppContext as _, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity,
12};
13use language::{Buffer, LanguageRegistry};
14use settings::WorktreeId;
15use std::sync::Arc;
16use text::Rope;
17use util::maybe;
18use worktree::{RepositoryEntry, StatusEntry};
19
20pub struct GitState {
21 repositories: Vec<RepositoryHandle>,
22 active_index: Option<usize>,
23 update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
24 languages: Arc<LanguageRegistry>,
25 _subscription: Subscription,
26}
27
28#[derive(Clone)]
29pub struct RepositoryHandle {
30 git_state: WeakEntity<GitState>,
31 worktree_id: WorktreeId,
32 repository_entry: RepositoryEntry,
33 git_repo: Option<Arc<dyn GitRepository>>,
34 commit_message: Entity<Buffer>,
35 update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
36}
37
38impl PartialEq<Self> for RepositoryHandle {
39 fn eq(&self, other: &Self) -> bool {
40 self.worktree_id == other.worktree_id
41 && self.repository_entry.work_directory_id()
42 == other.repository_entry.work_directory_id()
43 }
44}
45
46impl Eq for RepositoryHandle {}
47
48impl PartialEq<RepositoryEntry> for RepositoryHandle {
49 fn eq(&self, other: &RepositoryEntry) -> bool {
50 self.repository_entry.work_directory_id() == other.work_directory_id()
51 }
52}
53
54enum Message {
55 StageAndCommit(Arc<dyn GitRepository>, Rope, Vec<RepoPath>),
56 Commit(Arc<dyn GitRepository>, Rope),
57 Stage(Arc<dyn GitRepository>, Vec<RepoPath>),
58 Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
59}
60
61pub enum Event {
62 RepositoriesUpdated,
63}
64
65impl EventEmitter<Event> for GitState {}
66
67impl GitState {
68 pub fn new(
69 worktree_store: &Entity<WorktreeStore>,
70 languages: Arc<LanguageRegistry>,
71 cx: &mut Context<'_, Self>,
72 ) -> Self {
73 let (update_sender, mut update_receiver) =
74 mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
75 cx.spawn(|_, cx| async move {
76 while let Some((msg, mut err_sender)) = update_receiver.next().await {
77 let result = cx
78 .background_executor()
79 .spawn(async move {
80 match msg {
81 Message::StageAndCommit(repo, message, paths) => {
82 repo.stage_paths(&paths)?;
83 repo.commit(&message.to_string())?;
84 Ok(())
85 }
86 Message::Stage(repo, paths) => repo.stage_paths(&paths),
87 Message::Unstage(repo, paths) => repo.unstage_paths(&paths),
88 Message::Commit(repo, message) => repo.commit(&message.to_string()),
89 }
90 })
91 .await;
92 if let Err(e) = result {
93 err_sender.send(e).await.ok();
94 }
95 }
96 })
97 .detach();
98
99 let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
100
101 GitState {
102 languages,
103 repositories: Vec::new(),
104 active_index: None,
105 update_sender,
106 _subscription,
107 }
108 }
109
110 pub fn active_repository(&self) -> Option<RepositoryHandle> {
111 self.active_index
112 .map(|index| self.repositories[index].clone())
113 }
114
115 fn on_worktree_store_event(
116 &mut self,
117 worktree_store: Entity<WorktreeStore>,
118 _event: &WorktreeStoreEvent,
119 cx: &mut Context<'_, Self>,
120 ) {
121 // TODO inspect the event
122
123 let mut new_repositories = Vec::new();
124 let mut new_active_index = None;
125 let this = cx.weak_entity();
126
127 worktree_store.update(cx, |worktree_store, cx| {
128 for worktree in worktree_store.worktrees() {
129 worktree.update(cx, |worktree, cx| {
130 let snapshot = worktree.snapshot();
131 for repo in snapshot.repositories().iter() {
132 let git_repo = worktree
133 .as_local()
134 .and_then(|local_worktree| local_worktree.get_local_repo(repo))
135 .map(|local_repo| local_repo.repo().clone());
136 let existing = self
137 .repositories
138 .iter()
139 .enumerate()
140 .find(|(_, existing_handle)| existing_handle == &repo);
141 let handle = if let Some((index, handle)) = existing {
142 if self.active_index == Some(index) {
143 new_active_index = Some(new_repositories.len());
144 }
145 // Update the statuses but keep everything else.
146 let mut existing_handle = handle.clone();
147 existing_handle.repository_entry = repo.clone();
148 existing_handle
149 } else {
150 let commit_message = cx.new(|cx| Buffer::local("", cx));
151 cx.spawn({
152 let commit_message = commit_message.downgrade();
153 let languages = self.languages.clone();
154 |_, mut cx| async move {
155 let markdown = languages.language_for_name("Markdown").await?;
156 commit_message.update(&mut cx, |commit_message, cx| {
157 commit_message.set_language(Some(markdown), cx);
158 })?;
159 anyhow::Ok(())
160 }
161 })
162 .detach_and_log_err(cx);
163 RepositoryHandle {
164 git_state: this.clone(),
165 worktree_id: worktree.id(),
166 repository_entry: repo.clone(),
167 git_repo,
168 commit_message,
169 update_sender: self.update_sender.clone(),
170 }
171 };
172 new_repositories.push(handle);
173 }
174 })
175 }
176 });
177
178 if new_active_index == None && new_repositories.len() > 0 {
179 new_active_index = Some(0);
180 }
181
182 self.repositories = new_repositories;
183 self.active_index = new_active_index;
184
185 cx.emit(Event::RepositoriesUpdated);
186 }
187
188 pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
189 self.repositories.clone()
190 }
191}
192
193impl RepositoryHandle {
194 pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
195 maybe!({
196 let path = self.unrelativize(&"".into())?;
197 Some(
198 project
199 .absolute_path(&path, cx)?
200 .file_name()?
201 .to_string_lossy()
202 .to_string()
203 .into(),
204 )
205 })
206 .unwrap_or("".into())
207 }
208
209 pub fn activate(&self, cx: &mut App) {
210 let Some(git_state) = self.git_state.upgrade() else {
211 return;
212 };
213 git_state.update(cx, |git_state, cx| {
214 let Some((index, _)) = git_state
215 .repositories
216 .iter()
217 .enumerate()
218 .find(|(_, handle)| handle == &self)
219 else {
220 return;
221 };
222 git_state.active_index = Some(index);
223 cx.emit(Event::RepositoriesUpdated);
224 });
225 }
226
227 pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
228 self.repository_entry.status()
229 }
230
231 pub fn unrelativize(&self, path: &RepoPath) -> Option<ProjectPath> {
232 let path = self.repository_entry.unrelativize(path)?;
233 Some((self.worktree_id, path).into())
234 }
235
236 pub fn commit_message(&self) -> Entity<Buffer> {
237 self.commit_message.clone()
238 }
239
240 pub fn stage_entries(
241 &self,
242 entries: Vec<RepoPath>,
243 err_sender: mpsc::Sender<anyhow::Error>,
244 ) -> anyhow::Result<()> {
245 if entries.is_empty() {
246 return Ok(());
247 }
248 let Some(git_repo) = self.git_repo.clone() else {
249 return Ok(());
250 };
251 self.update_sender
252 .unbounded_send((Message::Stage(git_repo, entries), err_sender))
253 .map_err(|_| anyhow!("Failed to submit stage operation"))?;
254 Ok(())
255 }
256
257 pub fn unstage_entries(
258 &self,
259 entries: Vec<RepoPath>,
260 err_sender: mpsc::Sender<anyhow::Error>,
261 ) -> anyhow::Result<()> {
262 if entries.is_empty() {
263 return Ok(());
264 }
265 let Some(git_repo) = self.git_repo.clone() else {
266 return Ok(());
267 };
268 self.update_sender
269 .unbounded_send((Message::Unstage(git_repo, entries), err_sender))
270 .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
271 Ok(())
272 }
273
274 pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
275 let to_stage = self
276 .repository_entry
277 .status()
278 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
279 .map(|entry| entry.repo_path.clone())
280 .collect();
281 self.stage_entries(to_stage, err_sender)?;
282 Ok(())
283 }
284
285 pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
286 let to_unstage = self
287 .repository_entry
288 .status()
289 .filter(|entry| entry.status.is_staged().unwrap_or(true))
290 .map(|entry| entry.repo_path.clone())
291 .collect();
292 self.unstage_entries(to_unstage, err_sender)?;
293 Ok(())
294 }
295
296 /// Get a count of all entries in the active repository, including
297 /// untracked files.
298 pub fn entry_count(&self) -> usize {
299 self.repository_entry.status_len()
300 }
301
302 fn have_changes(&self) -> bool {
303 self.repository_entry.status_summary() != GitSummary::UNCHANGED
304 }
305
306 fn have_staged_changes(&self) -> bool {
307 self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
308 }
309
310 pub fn can_commit(&self, commit_all: bool, cx: &App) -> bool {
311 return self
312 .commit_message
313 .read(cx)
314 .chars()
315 .any(|c| !c.is_ascii_whitespace())
316 && self.have_changes()
317 && (commit_all || self.have_staged_changes());
318 }
319
320 pub fn commit(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
321 let Some(git_repo) = self.git_repo.clone() else {
322 return;
323 };
324 let message = self.commit_message.read(cx).as_rope().clone();
325 let result = self
326 .update_sender
327 .unbounded_send((Message::Commit(git_repo, message), err_sender.clone()));
328 if result.is_err() {
329 cx.spawn(|_| async move {
330 err_sender
331 .send(anyhow!("Failed to submit commit operation"))
332 .await
333 .ok();
334 })
335 .detach();
336 return;
337 }
338 self.commit_message.update(cx, |commit_message, cx| {
339 commit_message.set_text("", cx);
340 });
341 }
342
343 pub fn commit_all(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
344 let Some(git_repo) = self.git_repo.clone() else {
345 return;
346 };
347 let to_stage = self
348 .repository_entry
349 .status()
350 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
351 .map(|entry| entry.repo_path.clone())
352 .collect::<Vec<_>>();
353 let message = self.commit_message.read(cx).as_rope().clone();
354 let result = self.update_sender.unbounded_send((
355 Message::StageAndCommit(git_repo, message, to_stage),
356 err_sender.clone(),
357 ));
358 if result.is_err() {
359 cx.spawn(|_| async move {
360 err_sender
361 .send(anyhow!("Failed to submit commit all operation"))
362 .await
363 .ok();
364 })
365 .detach();
366 return;
367 }
368 self.commit_message.update(cx, |commit_message, cx| {
369 commit_message.set_text("", cx);
370 });
371 }
372}