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