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: 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![],
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 let Some(local) = worktree.as_local() else {
132 return;
133 };
134 for repo in snapshot.repositories().iter() {
135 let Some(local_repo) = local.get_local_repo(repo) else {
136 continue;
137 };
138 let existing = self
139 .repositories
140 .iter()
141 .enumerate()
142 .find(|(_, existing_handle)| existing_handle == &repo);
143 let handle = if let Some((index, handle)) = existing {
144 if self.active_index == Some(index) {
145 new_active_index = Some(new_repositories.len());
146 }
147 // Update the statuses but keep everything else.
148 let mut existing_handle = handle.clone();
149 existing_handle.repository_entry = repo.clone();
150 existing_handle
151 } else {
152 let commit_message = cx.new(|cx| Buffer::local("", cx));
153 cx.spawn({
154 let commit_message = commit_message.downgrade();
155 let languages = self.languages.clone();
156 |_, mut cx| async move {
157 let markdown = languages.language_for_name("Markdown").await?;
158 commit_message.update(&mut cx, |commit_message, cx| {
159 commit_message.set_language(Some(markdown), cx);
160 })?;
161 anyhow::Ok(())
162 }
163 })
164 .detach_and_log_err(cx);
165 RepositoryHandle {
166 git_state: this.clone(),
167 worktree_id: worktree.id(),
168 repository_entry: repo.clone(),
169 git_repo: local_repo.repo().clone(),
170 commit_message,
171 update_sender: self.update_sender.clone(),
172 }
173 };
174 new_repositories.push(handle);
175 }
176 })
177 }
178 });
179
180 if new_active_index == None && new_repositories.len() > 0 {
181 new_active_index = Some(0);
182 }
183
184 self.repositories = new_repositories;
185 self.active_index = new_active_index;
186
187 cx.emit(Event::RepositoriesUpdated);
188 }
189
190 pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
191 self.repositories.clone()
192 }
193}
194
195impl RepositoryHandle {
196 pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
197 maybe!({
198 let path = self.unrelativize(&"".into())?;
199 Some(
200 project
201 .absolute_path(&path, cx)?
202 .file_name()?
203 .to_string_lossy()
204 .to_string()
205 .into(),
206 )
207 })
208 .unwrap_or("".into())
209 }
210
211 pub fn activate(&self, cx: &mut App) {
212 let Some(git_state) = self.git_state.upgrade() else {
213 return;
214 };
215 git_state.update(cx, |git_state, cx| {
216 let Some((index, _)) = git_state
217 .repositories
218 .iter()
219 .enumerate()
220 .find(|(_, handle)| handle == &self)
221 else {
222 return;
223 };
224 git_state.active_index = Some(index);
225 cx.emit(Event::RepositoriesUpdated);
226 });
227 }
228
229 pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
230 self.repository_entry.status()
231 }
232
233 pub fn unrelativize(&self, path: &RepoPath) -> Option<ProjectPath> {
234 let path = self.repository_entry.unrelativize(path)?;
235 Some((self.worktree_id, path).into())
236 }
237
238 pub fn commit_message(&self) -> Entity<Buffer> {
239 self.commit_message.clone()
240 }
241
242 pub fn stage_entries(
243 &self,
244 entries: Vec<RepoPath>,
245 err_sender: mpsc::Sender<anyhow::Error>,
246 ) -> anyhow::Result<()> {
247 if entries.is_empty() {
248 return Ok(());
249 }
250 self.update_sender
251 .unbounded_send((Message::Stage(self.git_repo.clone(), entries), err_sender))
252 .map_err(|_| anyhow!("Failed to submit stage operation"))?;
253 Ok(())
254 }
255
256 pub fn unstage_entries(
257 &self,
258 entries: Vec<RepoPath>,
259 err_sender: mpsc::Sender<anyhow::Error>,
260 ) -> anyhow::Result<()> {
261 if entries.is_empty() {
262 return Ok(());
263 }
264 self.update_sender
265 .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), err_sender))
266 .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
267 Ok(())
268 }
269
270 pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
271 let to_stage = self
272 .repository_entry
273 .status()
274 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
275 .map(|entry| entry.repo_path.clone())
276 .collect();
277 self.stage_entries(to_stage, err_sender)?;
278 Ok(())
279 }
280
281 pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
282 let to_unstage = self
283 .repository_entry
284 .status()
285 .filter(|entry| entry.status.is_staged().unwrap_or(true))
286 .map(|entry| entry.repo_path.clone())
287 .collect();
288 self.unstage_entries(to_unstage, err_sender)?;
289 Ok(())
290 }
291
292 /// Get a count of all entries in the active repository, including
293 /// untracked files.
294 pub fn entry_count(&self) -> usize {
295 self.repository_entry.status_len()
296 }
297
298 fn have_changes(&self) -> bool {
299 self.repository_entry.status_summary() != GitSummary::UNCHANGED
300 }
301
302 fn have_staged_changes(&self) -> bool {
303 self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
304 }
305
306 pub fn can_commit(&self, commit_all: bool, cx: &App) -> bool {
307 return self
308 .commit_message
309 .read(cx)
310 .chars()
311 .any(|c| !c.is_ascii_whitespace())
312 && self.have_changes()
313 && (commit_all || self.have_staged_changes());
314 }
315
316 pub fn commit(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
317 let message = self.commit_message.read(cx).as_rope().clone();
318 let result = self.update_sender.unbounded_send((
319 Message::Commit(self.git_repo.clone(), message),
320 err_sender.clone(),
321 ));
322 if result.is_err() {
323 cx.spawn(|_| async move {
324 err_sender
325 .send(anyhow!("Failed to submit commit operation"))
326 .await
327 .ok();
328 })
329 .detach();
330 return;
331 }
332 self.commit_message.update(cx, |commit_message, cx| {
333 commit_message.set_text("", cx);
334 });
335 }
336
337 pub fn commit_all(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
338 let to_stage = self
339 .repository_entry
340 .status()
341 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
342 .map(|entry| entry.repo_path.clone())
343 .collect::<Vec<_>>();
344 let message = self.commit_message.read(cx).as_rope().clone();
345 let result = self.update_sender.unbounded_send((
346 Message::StageAndCommit(self.git_repo.clone(), message, to_stage),
347 err_sender.clone(),
348 ));
349 if result.is_err() {
350 cx.spawn(|_| async move {
351 err_sender
352 .send(anyhow!("Failed to submit commit all operation"))
353 .await
354 .ok();
355 })
356 .detach();
357 return;
358 }
359 self.commit_message.update(cx, |commit_message, cx| {
360 commit_message.set_text("", cx);
361 });
362 }
363}