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