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