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