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