1use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
2use crate::{Project, ProjectPath};
3use anyhow::{anyhow, Context as _};
4use client::ProjectId;
5use futures::channel::mpsc;
6use futures::{SinkExt as _, StreamExt as _};
7use git::{
8 repository::{GitRepository, RepoPath},
9 status::{GitSummary, TrackedSummary},
10};
11use gpui::{App, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity};
12use rpc::{proto, AnyProtoClient};
13use settings::WorktreeId;
14use std::sync::Arc;
15use util::{maybe, ResultExt};
16use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
17
18pub struct GitState {
19 project_id: Option<ProjectId>,
20 client: Option<AnyProtoClient>,
21 repositories: Vec<RepositoryHandle>,
22 active_index: Option<usize>,
23 update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
24 _subscription: Subscription,
25}
26
27#[derive(Clone)]
28pub struct RepositoryHandle {
29 git_state: WeakEntity<GitState>,
30 pub worktree_id: WorktreeId,
31 pub repository_entry: RepositoryEntry,
32 pub git_repo: GitRepo,
33 update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
34}
35
36#[derive(Clone)]
37pub enum GitRepo {
38 Local(Arc<dyn GitRepository>),
39 Remote {
40 project_id: ProjectId,
41 client: AnyProtoClient,
42 worktree_id: WorktreeId,
43 work_directory_id: ProjectEntryId,
44 },
45}
46
47impl PartialEq<Self> for RepositoryHandle {
48 fn eq(&self, other: &Self) -> bool {
49 self.worktree_id == other.worktree_id
50 && self.repository_entry.work_directory_id()
51 == other.repository_entry.work_directory_id()
52 }
53}
54
55impl Eq for RepositoryHandle {}
56
57impl PartialEq<RepositoryEntry> for RepositoryHandle {
58 fn eq(&self, other: &RepositoryEntry) -> bool {
59 self.repository_entry.work_directory_id() == other.work_directory_id()
60 }
61}
62
63enum Message {
64 StageAndCommit {
65 git_repo: GitRepo,
66 paths: Vec<RepoPath>,
67 name_and_email: Option<(SharedString, SharedString)>,
68 },
69 Commit {
70 git_repo: GitRepo,
71 name_and_email: Option<(SharedString, SharedString)>,
72 },
73 Stage(GitRepo, Vec<RepoPath>),
74 Unstage(GitRepo, Vec<RepoPath>),
75}
76
77pub enum Event {
78 RepositoriesUpdated,
79}
80
81impl EventEmitter<Event> for GitState {}
82
83impl GitState {
84 pub fn new(
85 worktree_store: &Entity<WorktreeStore>,
86 client: Option<AnyProtoClient>,
87 project_id: Option<ProjectId>,
88 cx: &mut Context<'_, Self>,
89 ) -> Self {
90 let (update_sender, mut update_receiver) =
91 mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
92 cx.spawn(|_, cx| async move {
93 while let Some((msg, mut err_sender)) = update_receiver.next().await {
94 let result =
95 cx.background_executor()
96 .spawn(async move {
97 match msg {
98 Message::StageAndCommit {
99 git_repo,
100 name_and_email,
101 paths,
102 } => {
103 match git_repo {
104 GitRepo::Local(repo) => {
105 repo.stage_paths(&paths)?;
106 repo.commit(name_and_email.as_ref().map(
107 |(name, email)| (name.as_ref(), email.as_ref()),
108 ))?;
109 }
110 GitRepo::Remote {
111 project_id,
112 client,
113 worktree_id,
114 work_directory_id,
115 } => {
116 client
117 .request(proto::Stage {
118 project_id: project_id.0,
119 worktree_id: worktree_id.to_proto(),
120 work_directory_id: work_directory_id.to_proto(),
121 paths: paths
122 .into_iter()
123 .map(|repo_path| repo_path.to_proto())
124 .collect(),
125 })
126 .await
127 .context("sending stage request")?;
128 let (name, email) = name_and_email.unzip();
129 client
130 .request(proto::Commit {
131 project_id: project_id.0,
132 worktree_id: worktree_id.to_proto(),
133 work_directory_id: work_directory_id.to_proto(),
134 name: name.map(String::from),
135 email: email.map(String::from),
136 })
137 .await
138 .context("sending commit request")?;
139 }
140 }
141
142 Ok(())
143 }
144 Message::Stage(repo, paths) => {
145 match repo {
146 GitRepo::Local(repo) => repo.stage_paths(&paths)?,
147 GitRepo::Remote {
148 project_id,
149 client,
150 worktree_id,
151 work_directory_id,
152 } => {
153 client
154 .request(proto::Stage {
155 project_id: project_id.0,
156 worktree_id: worktree_id.to_proto(),
157 work_directory_id: work_directory_id.to_proto(),
158 paths: paths
159 .into_iter()
160 .map(|repo_path| repo_path.to_proto())
161 .collect(),
162 })
163 .await
164 .context("sending stage request")?;
165 }
166 }
167 Ok(())
168 }
169 Message::Unstage(repo, paths) => {
170 match repo {
171 GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
172 GitRepo::Remote {
173 project_id,
174 client,
175 worktree_id,
176 work_directory_id,
177 } => {
178 client
179 .request(proto::Unstage {
180 project_id: project_id.0,
181 worktree_id: worktree_id.to_proto(),
182 work_directory_id: work_directory_id.to_proto(),
183 paths: paths
184 .into_iter()
185 .map(|repo_path| repo_path.to_proto())
186 .collect(),
187 })
188 .await
189 .context("sending unstage request")?;
190 }
191 }
192 Ok(())
193 }
194 Message::Commit {
195 git_repo,
196 name_and_email,
197 } => {
198 match git_repo {
199 GitRepo::Local(repo) => {
200 repo.commit(name_and_email.as_ref().map(
201 |(name, email)| (name.as_ref(), email.as_ref()),
202 ))?
203 }
204 GitRepo::Remote {
205 project_id,
206 client,
207 worktree_id,
208 work_directory_id,
209 } => {
210 let (name, email) = name_and_email.unzip();
211 client
212 .request(proto::Commit {
213 project_id: project_id.0,
214 worktree_id: worktree_id.to_proto(),
215 work_directory_id: work_directory_id.to_proto(),
216 name: name.map(String::from),
217 email: email.map(String::from),
218 })
219 .await
220 .context("sending commit request")?;
221 }
222 }
223 Ok(())
224 }
225 }
226 })
227 .await;
228 if let Err(e) = result {
229 err_sender.send(e).await.ok();
230 }
231 }
232 })
233 .detach();
234
235 let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
236
237 GitState {
238 project_id,
239 client,
240 repositories: Vec::new(),
241 active_index: None,
242 update_sender,
243 _subscription,
244 }
245 }
246
247 pub fn active_repository(&self) -> Option<RepositoryHandle> {
248 self.active_index
249 .map(|index| self.repositories[index].clone())
250 }
251
252 fn on_worktree_store_event(
253 &mut self,
254 worktree_store: Entity<WorktreeStore>,
255 _event: &WorktreeStoreEvent,
256 cx: &mut Context<'_, Self>,
257 ) {
258 // TODO inspect the event
259
260 let mut new_repositories = Vec::new();
261 let mut new_active_index = None;
262 let this = cx.weak_entity();
263 let client = self.client.clone();
264 let project_id = self.project_id;
265
266 worktree_store.update(cx, |worktree_store, cx| {
267 for worktree in worktree_store.worktrees() {
268 worktree.update(cx, |worktree, _| {
269 let snapshot = worktree.snapshot();
270 for repo in snapshot.repositories().iter() {
271 let git_repo = worktree
272 .as_local()
273 .and_then(|local_worktree| local_worktree.get_local_repo(repo))
274 .map(|local_repo| local_repo.repo().clone())
275 .map(GitRepo::Local)
276 .or_else(|| {
277 let client = client.clone()?;
278 let project_id = project_id?;
279 Some(GitRepo::Remote {
280 project_id,
281 client,
282 worktree_id: worktree.id(),
283 work_directory_id: repo.work_directory_id(),
284 })
285 });
286 let Some(git_repo) = git_repo else {
287 continue;
288 };
289 let existing = self
290 .repositories
291 .iter()
292 .enumerate()
293 .find(|(_, existing_handle)| existing_handle == &repo);
294 let handle = if let Some((index, handle)) = existing {
295 if self.active_index == Some(index) {
296 new_active_index = Some(new_repositories.len());
297 }
298 // Update the statuses but keep everything else.
299 let mut existing_handle = handle.clone();
300 existing_handle.repository_entry = repo.clone();
301 existing_handle
302 } else {
303 RepositoryHandle {
304 git_state: this.clone(),
305 worktree_id: worktree.id(),
306 repository_entry: repo.clone(),
307 git_repo,
308 update_sender: self.update_sender.clone(),
309 }
310 };
311 new_repositories.push(handle);
312 }
313 })
314 }
315 });
316
317 if new_active_index == None && new_repositories.len() > 0 {
318 new_active_index = Some(0);
319 }
320
321 self.repositories = new_repositories;
322 self.active_index = new_active_index;
323
324 cx.emit(Event::RepositoriesUpdated);
325 }
326
327 pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
328 self.repositories.clone()
329 }
330}
331
332impl RepositoryHandle {
333 pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
334 maybe!({
335 let path = self.repo_path_to_project_path(&"".into())?;
336 Some(
337 project
338 .absolute_path(&path, cx)?
339 .file_name()?
340 .to_string_lossy()
341 .to_string()
342 .into(),
343 )
344 })
345 .unwrap_or("".into())
346 }
347
348 pub fn activate(&self, cx: &mut App) {
349 let Some(git_state) = self.git_state.upgrade() else {
350 return;
351 };
352 git_state.update(cx, |git_state, cx| {
353 let Some((index, _)) = git_state
354 .repositories
355 .iter()
356 .enumerate()
357 .find(|(_, handle)| handle == &self)
358 else {
359 return;
360 };
361 git_state.active_index = Some(index);
362 cx.emit(Event::RepositoriesUpdated);
363 });
364 }
365
366 pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
367 self.repository_entry.status()
368 }
369
370 pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option<ProjectPath> {
371 let path = self.repository_entry.unrelativize(path)?;
372 Some((self.worktree_id, path).into())
373 }
374
375 pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
376 if path.worktree_id != self.worktree_id {
377 return None;
378 }
379 self.repository_entry.relativize(&path.path).log_err()
380 }
381
382 pub fn stage_entries(
383 &self,
384 entries: Vec<RepoPath>,
385 err_sender: mpsc::Sender<anyhow::Error>,
386 ) -> anyhow::Result<()> {
387 if entries.is_empty() {
388 return Ok(());
389 }
390 self.update_sender
391 .unbounded_send((Message::Stage(self.git_repo.clone(), entries), err_sender))
392 .map_err(|_| anyhow!("Failed to submit stage operation"))?;
393 Ok(())
394 }
395
396 pub fn unstage_entries(
397 &self,
398 entries: Vec<RepoPath>,
399 err_sender: mpsc::Sender<anyhow::Error>,
400 ) -> anyhow::Result<()> {
401 if entries.is_empty() {
402 return Ok(());
403 }
404 self.update_sender
405 .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), err_sender))
406 .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
407 Ok(())
408 }
409
410 pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
411 let to_stage = self
412 .repository_entry
413 .status()
414 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
415 .map(|entry| entry.repo_path.clone())
416 .collect();
417 self.stage_entries(to_stage, err_sender)?;
418 Ok(())
419 }
420
421 pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
422 let to_unstage = self
423 .repository_entry
424 .status()
425 .filter(|entry| entry.status.is_staged().unwrap_or(true))
426 .map(|entry| entry.repo_path.clone())
427 .collect();
428 self.unstage_entries(to_unstage, err_sender)?;
429 Ok(())
430 }
431
432 /// Get a count of all entries in the active repository, including
433 /// untracked files.
434 pub fn entry_count(&self) -> usize {
435 self.repository_entry.status_len()
436 }
437
438 fn have_changes(&self) -> bool {
439 self.repository_entry.status_summary() != GitSummary::UNCHANGED
440 }
441
442 fn have_staged_changes(&self) -> bool {
443 self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
444 }
445
446 pub fn can_commit(&self, commit_all: bool) -> bool {
447 return self.have_changes() && (commit_all || self.have_staged_changes());
448 }
449
450 pub fn commit(
451 &self,
452 name_and_email: Option<(SharedString, SharedString)>,
453 mut err_sender: mpsc::Sender<anyhow::Error>,
454 cx: &mut App,
455 ) -> anyhow::Result<()> {
456 let result = self.update_sender.unbounded_send((
457 Message::Commit {
458 git_repo: self.git_repo.clone(),
459 name_and_email,
460 },
461 err_sender.clone(),
462 ));
463 if result.is_err() {
464 cx.spawn(|_| async move {
465 err_sender
466 .send(anyhow!("Failed to submit commit operation"))
467 .await
468 .ok();
469 })
470 .detach();
471 anyhow::bail!("Failed to submit commit operation");
472 } else {
473 Ok(())
474 }
475 }
476
477 pub fn commit_all(
478 &self,
479 name_and_email: Option<(SharedString, SharedString)>,
480 mut err_sender: mpsc::Sender<anyhow::Error>,
481 cx: &mut App,
482 ) -> anyhow::Result<()> {
483 let to_stage = self
484 .repository_entry
485 .status()
486 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
487 .map(|entry| entry.repo_path.clone())
488 .collect();
489 let result = self.update_sender.unbounded_send((
490 Message::StageAndCommit {
491 git_repo: self.git_repo.clone(),
492 paths: to_stage,
493 name_and_email,
494 },
495 err_sender.clone(),
496 ));
497 if result.is_err() {
498 cx.spawn(|_| async move {
499 err_sender
500 .send(anyhow!("Failed to submit commit all operation"))
501 .await
502 .ok();
503 })
504 .detach();
505 anyhow::bail!("Failed to submit commit all operation");
506 } else {
507 Ok(())
508 }
509 }
510}