1use crate::buffer_store::BufferStore;
2use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
3use crate::{Project, ProjectPath};
4use anyhow::{Context as _, Result};
5use client::ProjectId;
6use futures::channel::{mpsc, oneshot};
7use futures::StreamExt as _;
8use git::repository::{Branch, CommitDetails, PushOptions, Remote, ResetMode};
9use git::{
10 repository::{GitRepository, RepoPath},
11 status::{GitSummary, TrackedSummary},
12};
13use gpui::{
14 App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription,
15 Task, WeakEntity,
16};
17use language::{Buffer, LanguageRegistry};
18use rpc::proto::{git_reset, ToProto};
19use rpc::{proto, AnyProtoClient, TypedEnvelope};
20use settings::WorktreeId;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23use text::BufferId;
24use util::{maybe, ResultExt};
25use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
26
27pub struct GitStore {
28 buffer_store: Entity<BufferStore>,
29 pub(super) project_id: Option<ProjectId>,
30 pub(super) client: Option<AnyProtoClient>,
31 repositories: Vec<Entity<Repository>>,
32 active_index: Option<usize>,
33 update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)>,
34 _subscription: Subscription,
35}
36
37pub struct Repository {
38 commit_message_buffer: Option<Entity<Buffer>>,
39 git_store: WeakEntity<GitStore>,
40 pub worktree_id: WorktreeId,
41 pub repository_entry: RepositoryEntry,
42 pub git_repo: GitRepo,
43 pub merge_message: Option<String>,
44 update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)>,
45}
46
47#[derive(Clone)]
48pub enum GitRepo {
49 Local(Arc<dyn GitRepository>),
50 Remote {
51 project_id: ProjectId,
52 client: AnyProtoClient,
53 worktree_id: WorktreeId,
54 work_directory_id: ProjectEntryId,
55 },
56}
57
58pub enum Message {
59 Commit {
60 git_repo: GitRepo,
61 message: SharedString,
62 name_and_email: Option<(SharedString, SharedString)>,
63 },
64 Reset {
65 repo: GitRepo,
66 commit: SharedString,
67 reset_mode: ResetMode,
68 },
69 CheckoutFiles {
70 repo: GitRepo,
71 commit: SharedString,
72 paths: Vec<RepoPath>,
73 },
74 Stage(GitRepo, Vec<RepoPath>),
75 Unstage(GitRepo, Vec<RepoPath>),
76 SetIndexText(GitRepo, RepoPath, Option<String>),
77 Push {
78 repo: GitRepo,
79 branch_name: SharedString,
80 remote_name: SharedString,
81 options: Option<PushOptions>,
82 },
83 Pull {
84 repo: GitRepo,
85 branch_name: SharedString,
86 remote_name: SharedString,
87 },
88 Fetch(GitRepo),
89}
90
91pub enum GitEvent {
92 ActiveRepositoryChanged,
93 FileSystemUpdated,
94 GitStateUpdated,
95}
96
97impl EventEmitter<GitEvent> for GitStore {}
98
99impl GitStore {
100 pub fn new(
101 worktree_store: &Entity<WorktreeStore>,
102 buffer_store: Entity<BufferStore>,
103 client: Option<AnyProtoClient>,
104 project_id: Option<ProjectId>,
105 cx: &mut Context<'_, Self>,
106 ) -> Self {
107 let update_sender = Self::spawn_git_worker(cx);
108 let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
109
110 GitStore {
111 project_id,
112 client,
113 buffer_store,
114 repositories: Vec::new(),
115 active_index: None,
116 update_sender,
117 _subscription,
118 }
119 }
120
121 pub fn init(client: &AnyProtoClient) {
122 client.add_entity_request_handler(Self::handle_get_remotes);
123 client.add_entity_request_handler(Self::handle_push);
124 client.add_entity_request_handler(Self::handle_pull);
125 client.add_entity_request_handler(Self::handle_fetch);
126 client.add_entity_request_handler(Self::handle_stage);
127 client.add_entity_request_handler(Self::handle_unstage);
128 client.add_entity_request_handler(Self::handle_commit);
129 client.add_entity_request_handler(Self::handle_reset);
130 client.add_entity_request_handler(Self::handle_show);
131 client.add_entity_request_handler(Self::handle_checkout_files);
132 client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
133 client.add_entity_request_handler(Self::handle_set_index_text);
134 }
135
136 pub fn active_repository(&self) -> Option<Entity<Repository>> {
137 self.active_index
138 .map(|index| self.repositories[index].clone())
139 }
140
141 fn on_worktree_store_event(
142 &mut self,
143 worktree_store: Entity<WorktreeStore>,
144 event: &WorktreeStoreEvent,
145 cx: &mut Context<'_, Self>,
146 ) {
147 let mut new_repositories = Vec::new();
148 let mut new_active_index = None;
149 let this = cx.weak_entity();
150 let client = self.client.clone();
151 let project_id = self.project_id;
152
153 worktree_store.update(cx, |worktree_store, cx| {
154 for worktree in worktree_store.worktrees() {
155 worktree.update(cx, |worktree, cx| {
156 let snapshot = worktree.snapshot();
157 for repo in snapshot.repositories().iter() {
158 let git_data = worktree
159 .as_local()
160 .and_then(|local_worktree| local_worktree.get_local_repo(repo))
161 .map(|local_repo| {
162 (
163 GitRepo::Local(local_repo.repo().clone()),
164 local_repo.merge_message.clone(),
165 )
166 })
167 .or_else(|| {
168 let client = client.clone()?;
169 let project_id = project_id?;
170 Some((
171 GitRepo::Remote {
172 project_id,
173 client,
174 worktree_id: worktree.id(),
175 work_directory_id: repo.work_directory_id(),
176 },
177 None,
178 ))
179 });
180 let Some((git_repo, merge_message)) = git_data else {
181 continue;
182 };
183 let worktree_id = worktree.id();
184 let existing =
185 self.repositories
186 .iter()
187 .enumerate()
188 .find(|(_, existing_handle)| {
189 existing_handle.read(cx).id()
190 == (worktree_id, repo.work_directory_id())
191 });
192 let handle = if let Some((index, handle)) = existing {
193 if self.active_index == Some(index) {
194 new_active_index = Some(new_repositories.len());
195 }
196 // Update the statuses and merge message but keep everything else.
197 let existing_handle = handle.clone();
198 existing_handle.update(cx, |existing_handle, cx| {
199 existing_handle.repository_entry = repo.clone();
200 if matches!(git_repo, GitRepo::Local { .. })
201 && existing_handle.merge_message != merge_message
202 {
203 if let (Some(merge_message), Some(buffer)) =
204 (&merge_message, &existing_handle.commit_message_buffer)
205 {
206 buffer.update(cx, |buffer, cx| {
207 if buffer.is_empty() {
208 buffer.set_text(merge_message.as_str(), cx);
209 }
210 })
211 }
212 existing_handle.merge_message = merge_message;
213 }
214 });
215 existing_handle
216 } else {
217 cx.new(|_| Repository {
218 git_store: this.clone(),
219 worktree_id,
220 repository_entry: repo.clone(),
221 git_repo,
222 update_sender: self.update_sender.clone(),
223 merge_message,
224 commit_message_buffer: None,
225 })
226 };
227 new_repositories.push(handle);
228 }
229 })
230 }
231 });
232
233 if new_active_index == None && new_repositories.len() > 0 {
234 new_active_index = Some(0);
235 }
236
237 self.repositories = new_repositories;
238 self.active_index = new_active_index;
239
240 match event {
241 WorktreeStoreEvent::WorktreeUpdatedGitRepositories(_) => {
242 cx.emit(GitEvent::GitStateUpdated);
243 }
244 _ => {
245 cx.emit(GitEvent::FileSystemUpdated);
246 }
247 }
248 }
249
250 pub fn all_repositories(&self) -> Vec<Entity<Repository>> {
251 self.repositories.clone()
252 }
253
254 fn spawn_git_worker(
255 cx: &mut Context<'_, GitStore>,
256 ) -> mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)> {
257 let (update_sender, mut update_receiver) =
258 mpsc::unbounded::<(Message, oneshot::Sender<Result<()>>)>();
259 cx.spawn(|_, cx| async move {
260 while let Some((msg, respond)) = update_receiver.next().await {
261 if !respond.is_canceled() {
262 let result = cx.background_spawn(Self::process_git_msg(msg)).await;
263 respond.send(result).ok();
264 }
265 }
266 })
267 .detach();
268 update_sender
269 }
270
271 async fn process_git_msg(msg: Message) -> Result<()> {
272 match msg {
273 Message::Fetch(repo) => {
274 match repo {
275 GitRepo::Local(git_repository) => git_repository.fetch()?,
276 GitRepo::Remote {
277 project_id,
278 client,
279 worktree_id,
280 work_directory_id,
281 } => {
282 client
283 .request(proto::Fetch {
284 project_id: project_id.0,
285 worktree_id: worktree_id.to_proto(),
286 work_directory_id: work_directory_id.to_proto(),
287 })
288 .await
289 .context("sending fetch request")?;
290 }
291 }
292 Ok(())
293 }
294
295 Message::Pull {
296 repo,
297 branch_name,
298 remote_name,
299 } => {
300 match repo {
301 GitRepo::Local(git_repository) => {
302 git_repository.pull(&branch_name, &remote_name)?
303 }
304 GitRepo::Remote {
305 project_id,
306 client,
307 worktree_id,
308 work_directory_id,
309 } => {
310 client
311 .request(proto::Pull {
312 project_id: project_id.0,
313 worktree_id: worktree_id.to_proto(),
314 work_directory_id: work_directory_id.to_proto(),
315 branch_name: branch_name.to_string(),
316 remote_name: remote_name.to_string(),
317 })
318 .await
319 .context("sending pull request")?;
320 }
321 }
322 Ok(())
323 }
324 Message::Push {
325 repo,
326 branch_name,
327 remote_name,
328 options,
329 } => {
330 match repo {
331 GitRepo::Local(git_repository) => {
332 git_repository.push(&branch_name, &remote_name, options)?
333 }
334 GitRepo::Remote {
335 project_id,
336 client,
337 worktree_id,
338 work_directory_id,
339 } => {
340 client
341 .request(proto::Push {
342 project_id: project_id.0,
343 worktree_id: worktree_id.to_proto(),
344 work_directory_id: work_directory_id.to_proto(),
345 branch_name: branch_name.to_string(),
346 remote_name: remote_name.to_string(),
347 options: options.map(|options| match options {
348 PushOptions::Force => proto::push::PushOptions::Force,
349 PushOptions::SetUpstream => {
350 proto::push::PushOptions::SetUpstream
351 }
352 }
353 as i32),
354 })
355 .await
356 .context("sending push request")?;
357 }
358 }
359 Ok(())
360 }
361 Message::Stage(repo, paths) => {
362 match repo {
363 GitRepo::Local(repo) => repo.stage_paths(&paths)?,
364 GitRepo::Remote {
365 project_id,
366 client,
367 worktree_id,
368 work_directory_id,
369 } => {
370 client
371 .request(proto::Stage {
372 project_id: project_id.0,
373 worktree_id: worktree_id.to_proto(),
374 work_directory_id: work_directory_id.to_proto(),
375 paths: paths
376 .into_iter()
377 .map(|repo_path| repo_path.as_ref().to_proto())
378 .collect(),
379 })
380 .await
381 .context("sending stage request")?;
382 }
383 }
384 Ok(())
385 }
386 Message::Reset {
387 repo,
388 commit,
389 reset_mode,
390 } => {
391 match repo {
392 GitRepo::Local(repo) => repo.reset(&commit, reset_mode)?,
393 GitRepo::Remote {
394 project_id,
395 client,
396 worktree_id,
397 work_directory_id,
398 } => {
399 client
400 .request(proto::GitReset {
401 project_id: project_id.0,
402 worktree_id: worktree_id.to_proto(),
403 work_directory_id: work_directory_id.to_proto(),
404 commit: commit.into(),
405 mode: match reset_mode {
406 ResetMode::Soft => git_reset::ResetMode::Soft.into(),
407 ResetMode::Mixed => git_reset::ResetMode::Mixed.into(),
408 },
409 })
410 .await?;
411 }
412 }
413 Ok(())
414 }
415
416 Message::CheckoutFiles {
417 repo,
418 commit,
419 paths,
420 } => {
421 match repo {
422 GitRepo::Local(repo) => repo.checkout_files(&commit, &paths)?,
423 GitRepo::Remote {
424 project_id,
425 client,
426 worktree_id,
427 work_directory_id,
428 } => {
429 client
430 .request(proto::GitCheckoutFiles {
431 project_id: project_id.0,
432 worktree_id: worktree_id.to_proto(),
433 work_directory_id: work_directory_id.to_proto(),
434 commit: commit.into(),
435 paths: paths
436 .into_iter()
437 .map(|p| p.to_string_lossy().to_string())
438 .collect(),
439 })
440 .await?;
441 }
442 }
443 Ok(())
444 }
445 Message::Unstage(repo, paths) => {
446 match repo {
447 GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
448 GitRepo::Remote {
449 project_id,
450 client,
451 worktree_id,
452 work_directory_id,
453 } => {
454 client
455 .request(proto::Unstage {
456 project_id: project_id.0,
457 worktree_id: worktree_id.to_proto(),
458 work_directory_id: work_directory_id.to_proto(),
459 paths: paths
460 .into_iter()
461 .map(|repo_path| repo_path.as_ref().to_proto())
462 .collect(),
463 })
464 .await
465 .context("sending unstage request")?;
466 }
467 }
468 Ok(())
469 }
470 Message::Commit {
471 git_repo,
472 message,
473 name_and_email,
474 } => {
475 match git_repo {
476 GitRepo::Local(repo) => repo.commit(
477 message.as_ref(),
478 name_and_email
479 .as_ref()
480 .map(|(name, email)| (name.as_ref(), email.as_ref())),
481 )?,
482 GitRepo::Remote {
483 project_id,
484 client,
485 worktree_id,
486 work_directory_id,
487 } => {
488 let (name, email) = name_and_email.unzip();
489 client
490 .request(proto::Commit {
491 project_id: project_id.0,
492 worktree_id: worktree_id.to_proto(),
493 work_directory_id: work_directory_id.to_proto(),
494 message: String::from(message),
495 name: name.map(String::from),
496 email: email.map(String::from),
497 })
498 .await
499 .context("sending commit request")?;
500 }
501 }
502 Ok(())
503 }
504 Message::SetIndexText(git_repo, path, text) => match git_repo {
505 GitRepo::Local(repo) => repo.set_index_text(&path, text),
506 GitRepo::Remote {
507 project_id,
508 client,
509 worktree_id,
510 work_directory_id,
511 } => client.send(proto::SetIndexText {
512 project_id: project_id.0,
513 worktree_id: worktree_id.to_proto(),
514 work_directory_id: work_directory_id.to_proto(),
515 path: path.as_ref().to_proto(),
516 text,
517 }),
518 },
519 }
520 }
521
522 async fn handle_fetch(
523 this: Entity<Self>,
524 envelope: TypedEnvelope<proto::Fetch>,
525 mut cx: AsyncApp,
526 ) -> Result<proto::Ack> {
527 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
528 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
529 let repository_handle =
530 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
531
532 repository_handle
533 .update(&mut cx, |repository_handle, _cx| repository_handle.fetch())?
534 .await??;
535 Ok(proto::Ack {})
536 }
537
538 async fn handle_push(
539 this: Entity<Self>,
540 envelope: TypedEnvelope<proto::Push>,
541 mut cx: AsyncApp,
542 ) -> Result<proto::Ack> {
543 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
544 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
545 let repository_handle =
546 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
547
548 let options = envelope
549 .payload
550 .options
551 .as_ref()
552 .map(|_| match envelope.payload.options() {
553 proto::push::PushOptions::SetUpstream => git::repository::PushOptions::SetUpstream,
554 proto::push::PushOptions::Force => git::repository::PushOptions::Force,
555 });
556
557 let branch_name = envelope.payload.branch_name.into();
558 let remote_name = envelope.payload.remote_name.into();
559
560 repository_handle
561 .update(&mut cx, |repository_handle, _cx| {
562 repository_handle.push(branch_name, remote_name, options)
563 })?
564 .await??;
565 Ok(proto::Ack {})
566 }
567
568 async fn handle_pull(
569 this: Entity<Self>,
570 envelope: TypedEnvelope<proto::Pull>,
571 mut cx: AsyncApp,
572 ) -> Result<proto::Ack> {
573 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
574 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
575 let repository_handle =
576 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
577
578 let branch_name = envelope.payload.branch_name.into();
579 let remote_name = envelope.payload.remote_name.into();
580
581 repository_handle
582 .update(&mut cx, |repository_handle, _cx| {
583 repository_handle.pull(branch_name, remote_name)
584 })?
585 .await??;
586 Ok(proto::Ack {})
587 }
588
589 async fn handle_stage(
590 this: Entity<Self>,
591 envelope: TypedEnvelope<proto::Stage>,
592 mut cx: AsyncApp,
593 ) -> Result<proto::Ack> {
594 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
595 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
596 let repository_handle =
597 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
598
599 let entries = envelope
600 .payload
601 .paths
602 .into_iter()
603 .map(PathBuf::from)
604 .map(RepoPath::new)
605 .collect();
606
607 repository_handle
608 .update(&mut cx, |repository_handle, cx| {
609 repository_handle.stage_entries(entries, cx)
610 })?
611 .await?;
612 Ok(proto::Ack {})
613 }
614
615 async fn handle_unstage(
616 this: Entity<Self>,
617 envelope: TypedEnvelope<proto::Unstage>,
618 mut cx: AsyncApp,
619 ) -> Result<proto::Ack> {
620 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
621 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
622 let repository_handle =
623 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
624
625 let entries = envelope
626 .payload
627 .paths
628 .into_iter()
629 .map(PathBuf::from)
630 .map(RepoPath::new)
631 .collect();
632
633 repository_handle
634 .update(&mut cx, |repository_handle, cx| {
635 repository_handle.unstage_entries(entries, cx)
636 })?
637 .await?;
638
639 Ok(proto::Ack {})
640 }
641
642 async fn handle_set_index_text(
643 this: Entity<Self>,
644 envelope: TypedEnvelope<proto::SetIndexText>,
645 mut cx: AsyncApp,
646 ) -> Result<proto::Ack> {
647 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
648 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
649 let repository_handle =
650 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
651
652 repository_handle
653 .update(&mut cx, |repository_handle, _| {
654 repository_handle.set_index_text(
655 &RepoPath::from_str(&envelope.payload.path),
656 envelope.payload.text,
657 )
658 })?
659 .await??;
660 Ok(proto::Ack {})
661 }
662
663 async fn handle_commit(
664 this: Entity<Self>,
665 envelope: TypedEnvelope<proto::Commit>,
666 mut cx: AsyncApp,
667 ) -> Result<proto::Ack> {
668 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
669 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
670 let repository_handle =
671 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
672
673 let message = SharedString::from(envelope.payload.message);
674 let name = envelope.payload.name.map(SharedString::from);
675 let email = envelope.payload.email.map(SharedString::from);
676
677 repository_handle
678 .update(&mut cx, |repository_handle, _| {
679 repository_handle.commit(message, name.zip(email))
680 })?
681 .await??;
682 Ok(proto::Ack {})
683 }
684
685 async fn handle_get_remotes(
686 this: Entity<Self>,
687 envelope: TypedEnvelope<proto::GetRemotes>,
688 mut cx: AsyncApp,
689 ) -> Result<proto::GetRemotesResponse> {
690 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
691 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
692 let repository_handle =
693 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
694
695 let branch_name = envelope.payload.branch_name;
696
697 let remotes = repository_handle
698 .update(&mut cx, |repository_handle, cx| {
699 repository_handle.get_remotes(branch_name, cx)
700 })?
701 .await?;
702
703 Ok(proto::GetRemotesResponse {
704 remotes: remotes
705 .into_iter()
706 .map(|remotes| proto::get_remotes_response::Remote {
707 name: remotes.name.to_string(),
708 })
709 .collect::<Vec<_>>(),
710 })
711 }
712
713 async fn handle_show(
714 this: Entity<Self>,
715 envelope: TypedEnvelope<proto::GitShow>,
716 mut cx: AsyncApp,
717 ) -> Result<proto::GitCommitDetails> {
718 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
719 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
720 let repository_handle =
721 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
722
723 let commit = repository_handle
724 .update(&mut cx, |repository_handle, cx| {
725 repository_handle.show(&envelope.payload.commit, cx)
726 })?
727 .await?;
728 Ok(proto::GitCommitDetails {
729 sha: commit.sha.into(),
730 message: commit.message.into(),
731 commit_timestamp: commit.commit_timestamp,
732 committer_email: commit.committer_email.into(),
733 committer_name: commit.committer_name.into(),
734 })
735 }
736
737 async fn handle_reset(
738 this: Entity<Self>,
739 envelope: TypedEnvelope<proto::GitReset>,
740 mut cx: AsyncApp,
741 ) -> Result<proto::Ack> {
742 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
743 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
744 let repository_handle =
745 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
746
747 let mode = match envelope.payload.mode() {
748 git_reset::ResetMode::Soft => ResetMode::Soft,
749 git_reset::ResetMode::Mixed => ResetMode::Mixed,
750 };
751
752 repository_handle
753 .update(&mut cx, |repository_handle, _| {
754 repository_handle.reset(&envelope.payload.commit, mode)
755 })?
756 .await??;
757 Ok(proto::Ack {})
758 }
759
760 async fn handle_checkout_files(
761 this: Entity<Self>,
762 envelope: TypedEnvelope<proto::GitCheckoutFiles>,
763 mut cx: AsyncApp,
764 ) -> Result<proto::Ack> {
765 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
766 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
767 let repository_handle =
768 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
769 let paths = envelope
770 .payload
771 .paths
772 .iter()
773 .map(|s| RepoPath::from_str(s))
774 .collect();
775
776 repository_handle
777 .update(&mut cx, |repository_handle, _| {
778 repository_handle.checkout_files(&envelope.payload.commit, paths)
779 })?
780 .await??;
781 Ok(proto::Ack {})
782 }
783
784 async fn handle_open_commit_message_buffer(
785 this: Entity<Self>,
786 envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
787 mut cx: AsyncApp,
788 ) -> Result<proto::OpenBufferResponse> {
789 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
790 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
791 let repository =
792 Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
793 let buffer = repository
794 .update(&mut cx, |repository, cx| {
795 repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
796 })?
797 .await?;
798
799 let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?;
800 this.update(&mut cx, |this, cx| {
801 this.buffer_store.update(cx, |buffer_store, cx| {
802 buffer_store
803 .create_buffer_for_peer(
804 &buffer,
805 envelope.original_sender_id.unwrap_or(envelope.sender_id),
806 cx,
807 )
808 .detach_and_log_err(cx);
809 })
810 })?;
811
812 Ok(proto::OpenBufferResponse {
813 buffer_id: buffer_id.to_proto(),
814 })
815 }
816
817 fn repository_for_request(
818 this: &Entity<Self>,
819 worktree_id: WorktreeId,
820 work_directory_id: ProjectEntryId,
821 cx: &mut AsyncApp,
822 ) -> Result<Entity<Repository>> {
823 this.update(cx, |this, cx| {
824 let repository_handle = this
825 .all_repositories()
826 .into_iter()
827 .find(|repository_handle| {
828 repository_handle.read(cx).worktree_id == worktree_id
829 && repository_handle
830 .read(cx)
831 .repository_entry
832 .work_directory_id()
833 == work_directory_id
834 })
835 .context("missing repository handle")?;
836 anyhow::Ok(repository_handle)
837 })?
838 }
839}
840
841impl GitRepo {}
842
843impl Repository {
844 pub fn git_store(&self) -> Option<Entity<GitStore>> {
845 self.git_store.upgrade()
846 }
847
848 fn id(&self) -> (WorktreeId, ProjectEntryId) {
849 (self.worktree_id, self.repository_entry.work_directory_id())
850 }
851
852 pub fn current_branch(&self) -> Option<&Branch> {
853 self.repository_entry.branch()
854 }
855
856 pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
857 maybe!({
858 let project_path = self.repo_path_to_project_path(&"".into())?;
859 let worktree_name = project
860 .worktree_for_id(project_path.worktree_id, cx)?
861 .read(cx)
862 .root_name();
863
864 let mut path = PathBuf::new();
865 path = path.join(worktree_name);
866 path = path.join(project_path.path);
867 Some(path.to_string_lossy().to_string())
868 })
869 .unwrap_or_else(|| self.repository_entry.work_directory.display_name())
870 .into()
871 }
872
873 pub fn activate(&self, cx: &mut Context<Self>) {
874 let Some(git_store) = self.git_store.upgrade() else {
875 return;
876 };
877 let entity = cx.entity();
878 git_store.update(cx, |git_store, cx| {
879 let Some(index) = git_store
880 .repositories
881 .iter()
882 .position(|handle| *handle == entity)
883 else {
884 return;
885 };
886 git_store.active_index = Some(index);
887 cx.emit(GitEvent::ActiveRepositoryChanged);
888 });
889 }
890
891 pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
892 self.repository_entry.status()
893 }
894
895 pub fn has_conflict(&self, path: &RepoPath) -> bool {
896 self.repository_entry
897 .current_merge_conflicts
898 .contains(&path)
899 }
900
901 pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option<ProjectPath> {
902 let path = self.repository_entry.unrelativize(path)?;
903 Some((self.worktree_id, path).into())
904 }
905
906 pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
907 self.worktree_id_path_to_repo_path(path.worktree_id, &path.path)
908 }
909
910 pub fn worktree_id_path_to_repo_path(
911 &self,
912 worktree_id: WorktreeId,
913 path: &Path,
914 ) -> Option<RepoPath> {
915 if worktree_id != self.worktree_id {
916 return None;
917 }
918 self.repository_entry.relativize(path).log_err()
919 }
920
921 pub fn open_commit_buffer(
922 &mut self,
923 languages: Option<Arc<LanguageRegistry>>,
924 buffer_store: Entity<BufferStore>,
925 cx: &mut Context<Self>,
926 ) -> Task<Result<Entity<Buffer>>> {
927 if let Some(buffer) = self.commit_message_buffer.clone() {
928 return Task::ready(Ok(buffer));
929 }
930
931 if let GitRepo::Remote {
932 project_id,
933 client,
934 worktree_id,
935 work_directory_id,
936 } = self.git_repo.clone()
937 {
938 let client = client.clone();
939 cx.spawn(|repository, mut cx| async move {
940 let request = client.request(proto::OpenCommitMessageBuffer {
941 project_id: project_id.0,
942 worktree_id: worktree_id.to_proto(),
943 work_directory_id: work_directory_id.to_proto(),
944 });
945 let response = request.await.context("requesting to open commit buffer")?;
946 let buffer_id = BufferId::new(response.buffer_id)?;
947 let buffer = buffer_store
948 .update(&mut cx, |buffer_store, cx| {
949 buffer_store.wait_for_remote_buffer(buffer_id, cx)
950 })?
951 .await?;
952 if let Some(language_registry) = languages {
953 let git_commit_language =
954 language_registry.language_for_name("Git Commit").await?;
955 buffer.update(&mut cx, |buffer, cx| {
956 buffer.set_language(Some(git_commit_language), cx);
957 })?;
958 }
959 repository.update(&mut cx, |repository, _| {
960 repository.commit_message_buffer = Some(buffer.clone());
961 })?;
962 Ok(buffer)
963 })
964 } else {
965 self.open_local_commit_buffer(languages, buffer_store, cx)
966 }
967 }
968
969 fn open_local_commit_buffer(
970 &mut self,
971 language_registry: Option<Arc<LanguageRegistry>>,
972 buffer_store: Entity<BufferStore>,
973 cx: &mut Context<Self>,
974 ) -> Task<Result<Entity<Buffer>>> {
975 let merge_message = self.merge_message.clone();
976 cx.spawn(|repository, mut cx| async move {
977 let buffer = buffer_store
978 .update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
979 .await?;
980
981 if let Some(language_registry) = language_registry {
982 let git_commit_language = language_registry.language_for_name("Git Commit").await?;
983 buffer.update(&mut cx, |buffer, cx| {
984 buffer.set_language(Some(git_commit_language), cx);
985 })?;
986 }
987
988 if let Some(merge_message) = merge_message {
989 buffer.update(&mut cx, |buffer, cx| {
990 buffer.set_text(merge_message.as_str(), cx)
991 })?;
992 }
993
994 repository.update(&mut cx, |repository, _| {
995 repository.commit_message_buffer = Some(buffer.clone());
996 })?;
997 Ok(buffer)
998 })
999 }
1000
1001 pub fn checkout_files(
1002 &self,
1003 commit: &str,
1004 paths: Vec<RepoPath>,
1005 ) -> oneshot::Receiver<Result<()>> {
1006 self.send_message(Message::CheckoutFiles {
1007 repo: self.git_repo.clone(),
1008 commit: commit.to_string().into(),
1009 paths,
1010 })
1011 }
1012
1013 pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
1014 self.send_message(Message::Reset {
1015 repo: self.git_repo.clone(),
1016 commit: commit.to_string().into(),
1017 reset_mode,
1018 })
1019 }
1020
1021 pub fn show(&self, commit: &str, cx: &Context<Self>) -> Task<Result<CommitDetails>> {
1022 let commit = commit.to_string();
1023 match self.git_repo.clone() {
1024 GitRepo::Local(git_repository) => {
1025 let commit = commit.to_string();
1026 cx.background_spawn(async move { git_repository.show(&commit) })
1027 }
1028 GitRepo::Remote {
1029 project_id,
1030 client,
1031 worktree_id,
1032 work_directory_id,
1033 } => cx.background_spawn(async move {
1034 let resp = client
1035 .request(proto::GitShow {
1036 project_id: project_id.0,
1037 worktree_id: worktree_id.to_proto(),
1038 work_directory_id: work_directory_id.to_proto(),
1039 commit,
1040 })
1041 .await?;
1042
1043 Ok(CommitDetails {
1044 sha: resp.sha.into(),
1045 message: resp.message.into(),
1046 commit_timestamp: resp.commit_timestamp,
1047 committer_email: resp.committer_email.into(),
1048 committer_name: resp.committer_name.into(),
1049 })
1050 }),
1051 }
1052 }
1053
1054 fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
1055 Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
1056 }
1057
1058 pub fn stage_entries(&self, entries: Vec<RepoPath>, cx: &mut App) -> Task<anyhow::Result<()>> {
1059 let (result_tx, result_rx) = futures::channel::oneshot::channel();
1060 if entries.is_empty() {
1061 return Task::ready(Ok(()));
1062 }
1063
1064 let mut save_futures = Vec::new();
1065 if let Some(buffer_store) = self.buffer_store(cx) {
1066 buffer_store.update(cx, |buffer_store, cx| {
1067 for path in &entries {
1068 let Some(path) = self.repository_entry.unrelativize(path) else {
1069 continue;
1070 };
1071 let project_path = (self.worktree_id, path).into();
1072 if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) {
1073 save_futures.push(buffer_store.save_buffer(buffer, cx));
1074 }
1075 }
1076 })
1077 }
1078
1079 let update_sender = self.update_sender.clone();
1080 let git_repo = self.git_repo.clone();
1081 cx.spawn(|_| async move {
1082 for save_future in save_futures {
1083 save_future.await?;
1084 }
1085 update_sender
1086 .unbounded_send((Message::Stage(git_repo, entries), result_tx))
1087 .ok();
1088 result_rx.await.anyhow()??;
1089 Ok(())
1090 })
1091 }
1092
1093 pub fn unstage_entries(
1094 &self,
1095 entries: Vec<RepoPath>,
1096 cx: &mut App,
1097 ) -> Task<anyhow::Result<()>> {
1098 let (result_tx, result_rx) = futures::channel::oneshot::channel();
1099 if entries.is_empty() {
1100 return Task::ready(Ok(()));
1101 }
1102
1103 let mut save_futures = Vec::new();
1104 if let Some(buffer_store) = self.buffer_store(cx) {
1105 buffer_store.update(cx, |buffer_store, cx| {
1106 for path in &entries {
1107 let Some(path) = self.repository_entry.unrelativize(path) else {
1108 continue;
1109 };
1110 let project_path = (self.worktree_id, path).into();
1111 if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) {
1112 save_futures.push(buffer_store.save_buffer(buffer, cx));
1113 }
1114 }
1115 })
1116 }
1117
1118 let update_sender = self.update_sender.clone();
1119 let git_repo = self.git_repo.clone();
1120 cx.spawn(|_| async move {
1121 for save_future in save_futures {
1122 save_future.await?;
1123 }
1124 update_sender
1125 .unbounded_send((Message::Unstage(git_repo, entries), result_tx))
1126 .ok();
1127 result_rx.await.anyhow()??;
1128 Ok(())
1129 })
1130 }
1131
1132 pub fn stage_all(&self, cx: &mut App) -> Task<anyhow::Result<()>> {
1133 let to_stage = self
1134 .repository_entry
1135 .status()
1136 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
1137 .map(|entry| entry.repo_path.clone())
1138 .collect();
1139 self.stage_entries(to_stage, cx)
1140 }
1141
1142 pub fn unstage_all(&self, cx: &mut App) -> Task<anyhow::Result<()>> {
1143 let to_unstage = self
1144 .repository_entry
1145 .status()
1146 .filter(|entry| entry.status.is_staged().unwrap_or(true))
1147 .map(|entry| entry.repo_path.clone())
1148 .collect();
1149 self.unstage_entries(to_unstage, cx)
1150 }
1151
1152 /// Get a count of all entries in the active repository, including
1153 /// untracked files.
1154 pub fn entry_count(&self) -> usize {
1155 self.repository_entry.status_len()
1156 }
1157
1158 fn have_changes(&self) -> bool {
1159 self.repository_entry.status_summary() != GitSummary::UNCHANGED
1160 }
1161
1162 fn have_staged_changes(&self) -> bool {
1163 self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
1164 }
1165
1166 pub fn can_commit(&self, commit_all: bool) -> bool {
1167 return self.have_changes() && (commit_all || self.have_staged_changes());
1168 }
1169
1170 pub fn commit(
1171 &self,
1172 message: SharedString,
1173 name_and_email: Option<(SharedString, SharedString)>,
1174 ) -> oneshot::Receiver<Result<()>> {
1175 self.send_message(Message::Commit {
1176 git_repo: self.git_repo.clone(),
1177 message,
1178 name_and_email,
1179 })
1180 }
1181
1182 pub fn fetch(&self) -> oneshot::Receiver<Result<()>> {
1183 self.send_message(Message::Fetch(self.git_repo.clone()))
1184 }
1185
1186 pub fn push(
1187 &self,
1188 branch: SharedString,
1189 remote: SharedString,
1190 options: Option<PushOptions>,
1191 ) -> oneshot::Receiver<Result<()>> {
1192 self.send_message(Message::Push {
1193 repo: self.git_repo.clone(),
1194 branch_name: branch,
1195 remote_name: remote,
1196 options,
1197 })
1198 }
1199
1200 pub fn pull(
1201 &self,
1202 branch: SharedString,
1203 remote: SharedString,
1204 ) -> oneshot::Receiver<Result<()>> {
1205 self.send_message(Message::Pull {
1206 repo: self.git_repo.clone(),
1207 branch_name: branch,
1208 remote_name: remote,
1209 })
1210 }
1211
1212 pub fn set_index_text(
1213 &self,
1214 path: &RepoPath,
1215 content: Option<String>,
1216 ) -> oneshot::Receiver<anyhow::Result<()>> {
1217 self.send_message(Message::SetIndexText(
1218 self.git_repo.clone(),
1219 path.clone(),
1220 content,
1221 ))
1222 }
1223
1224 pub fn get_remotes(&self, branch_name: Option<String>, cx: &App) -> Task<Result<Vec<Remote>>> {
1225 match self.git_repo.clone() {
1226 GitRepo::Local(git_repository) => {
1227 cx.background_spawn(
1228 async move { git_repository.get_remotes(branch_name.as_deref()) },
1229 )
1230 }
1231 GitRepo::Remote {
1232 project_id,
1233 client,
1234 worktree_id,
1235 work_directory_id,
1236 } => cx.background_spawn(async move {
1237 let response = client
1238 .request(proto::GetRemotes {
1239 project_id: project_id.0,
1240 worktree_id: worktree_id.to_proto(),
1241 work_directory_id: work_directory_id.to_proto(),
1242 branch_name,
1243 })
1244 .await?;
1245
1246 Ok(response
1247 .remotes
1248 .into_iter()
1249 .map(|remotes| git::repository::Remote {
1250 name: remotes.name.into(),
1251 })
1252 .collect())
1253 }),
1254 }
1255 }
1256
1257 fn send_message(&self, message: Message) -> oneshot::Receiver<anyhow::Result<()>> {
1258 let (result_tx, result_rx) = futures::channel::oneshot::channel();
1259 self.update_sender.unbounded_send((message, result_tx)).ok();
1260 result_rx
1261 }
1262}