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