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