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